-
Notifications
You must be signed in to change notification settings - Fork 3
Automation
Automation exposes a fixed set of Joro operations to a local automation client over MCP, each one checked against a token's grants before it runs. Joro is the server: it embeds no model and makes no outbound AI calls, and it holds no provider credentials.
Everything here lives under Settings → Automation, and applies to proxy mode only.
The MCP server card holds an Enabled checkbox and a Port, which defaults to 9091 — one above the UI port. The port commits when the field loses focus. A status chip beside them reads running, stopped, or error: followed by the operating system's own message, and the endpoint appears once it is up.
The listener is off by default, binds 127.0.0.1 only, and speaks plain HTTP. It is not governed by --bind and takes no certificate — an automation client is a local process, and a listener reachable from the network is a different threat model than this one is built for.
It refuses to start until at least one token exists. An MCP server with no tokens would accept any local process, which makes it a privilege-escalation gadget for everything else running as you.
The port cannot collide with the UI or proxy port. If the bind fails, the error you get back is the real one from the OS, and Enabled is not saved — so a restart never silently retries a port you never saw fail. Once it does start, the enabled state and port persist in ~/.joro/automation.json and the listener comes back on its own next launch.
Automation is proxy-mode only. A Listener-Mode or Team-Server instance never registers the routes or the second port.
The endpoint is http://127.0.0.1:9091/mcp, POST-only, with the token in an Authorization: Bearer header. Joro builds the client configuration for you when it shows the secret, behind Copy config with secret:
{
"mcpServers": {
"joro": {
"url": "http://127.0.0.1:9091/mcp",
"headers": {
"Authorization": "Bearer joro_<secret>"
}
}
}
}The equivalent for Claude Code:
claude mcp add --transport http joro http://127.0.0.1:9091/mcp \
--header "Authorization: Bearer joro_<secret>"Some things about the transport will bite you if you do not know them:
-
The guard is stricter than the UI's. The
Hostheader must be a loopback name, with no--allowed-hostescape. AnyOriginheader at all is a 403, as is aSec-Fetch-Siteother thannone— a real MCP client is not a browser, so the presence of those headers is itself the signal. This is anti-rebinding, and it runs before authentication. A browser-based client cannot connect, by design. -
GET /mcpreturns 405. The server initiates no messages, so there is no event stream to subscribe to. - A grant change does not reach a connected client. Because nothing is pushed, the client picks up an edited grant list on its next tool listing, or on reconnect.
-
Tool names are capability IDs with dots as underscores:
history.listishistory_list,config.noise.editisconfig_noise_edit. There is nojoro_prefix on tool names — the client already namespaces by server, and every name sits in the model's context on every turn. -
tools/listis filtered by grant. An ungranted capability is not merely refused, it is never named, so it costs no context and does not invite an attempt. -
A tool failure comes back as a successful response flagged
isError. That keeps the failure in the model's conversation where it can correct itself, rather than being swallowed by the client's transport.
A wrong or missing token is a 401 with the same body either way, so the endpoint cannot be used to confirm that a token exists. A disabled token gets this token is disabled and an expired one this token has expired, both 403 — they are distinguishable only because they take a correct secret to reach.
New token opens the editor:
- Name: how the token is identified in the table and in Activity. Up to 64 characters.
- Capabilities: the grant list, chosen in the picker described below.
- Requests / minute: default 60, from 1 to 600. The bucket's burst capacity equals the rate, so a client can open with a fan-out and then settle.
- Max concurrent: default 2, from 1 to 16. A separate global cap of 8 across all tokens is not adjustable — it exists so automation cannot starve your own browsing.
- Expires in (days, 0 = never): up to 365. Set at creation only; editing a token cannot change it.
- Only allow sends to in-scope targets: on by default. Sends are refused unless scope is enabled, has at least one rule, and matches the target.
- Host whitelist: optional, comma-separated globs. Combined with the scope check, never instead of it.
- Show credential header values: off by default. On, every session token in captured traffic is readable by this token.
Save stays disabled until the token has a name and at least one grant.
The secret is shown once, in the only place in Joro where a plaintext token ever appears. Joro keeps a SHA-256 of it and nothing else, so a lost secret is replaced by rotating, not recovered.
Token shows the name and joro_ plus the first eight characters of the secret, which is all Joro retains for display. Grants is a count, with the full list on hover and a warning marker when any grant sends traffic. Limits reads as 60/min · 2 conc. Last used reports the timestamp and the last capability called, including failed attempts.
Two markers in the Scope column are worth recognizing:
-
N grant(s) inert: the token holds a scope-write capability while being restricted by scope or a host whitelist, so those grants are refused on every call. Either drop them or lift the restriction. -
credentials visible:Authorization,Cookieand similar values are returned to this token in full.
A N not granted → link appears when capabilities have shipped that this token has never been offered. Nothing is ever granted implicitly; the link opens the editor so you can decide, and Reviewed dismisses it without granting anything.
Row actions are Edit, Rotate, Enable/Disable, and Revoke. Rotating kills the old secret the moment it completes — there is no overlap window, so a client using it fails on its next request. Revoking cannot be undone, but Activity keeps the token's history because the name is recorded on each entry. Rotate, disable and revoke all drop the token's session cookies.
~/.joro/automation.json, mode 0600, written to a temporary file and renamed. It is deliberately in neither the project config nor the user config:
- A project config is published to teammates, so a grant set and an auto-starting listener riding along would be a real privilege transfer.
- A user config round-trips through save, load and export, with version-gated backfill of new defaults. Security state must not inherit "helpfully add the new default" semantics.
A corrupt or hand-mangled file disables automation for that run and says so in the log, rather than resetting itself. Reserved grants in a hand-edited file are dropped on load.
There are 49 capabilities, or 41 with neither launch flag: --automation-privileged adds five, --automation-scripting adds three. Fourteen of the forty-one change something.
Grants are always fully expanded capability IDs. Wildcards do not exist. An http.* written today would silently grant a send-capable capability shipped in a later release. For the same reason, a capability added to Joro after a token was issued is surfaced against that token rather than picked up by it.
The picker groups capabilities by class in the server's own order rather than alphabetically, so the write-heavy configuration and detection groups come last. Above the groups sit the five Profiles, then Read-only / Everything / Clear presets. Each row shows the tool name, what it does, and markers for the flags below. Ticking a privileged capability opens a second confirmation over the editor, which does not confirm on Enter.
In the tables that follow, the flags mean:
- sends traffic: emits real requests to a target, and is held to the scope guard.
- changes Joro: alters your proxy, your project, or your records. Every one of these is itemized in Activity.
-
privileged: exists only under a launch flag —
--automation-privilegedfor execution and C2,--automation-scriptingfor the script capabilities — and is in no profile. - scope-exempt only: refused unless the token has the scope requirement off and no host whitelist.
| Tool | What it does | Flags |
|---|---|---|
instance_get |
Describes this instance in one call: version, proxy address, active project, capture count, scope and intercept state, detect state and finding count. | |
history_list |
Lists captured requests as a compact table. Returns no bodies. | |
history_stats |
Counts by status class and by host — the cheapest way to get oriented. | |
history_highlight |
Sets or clears a row highlight on a captured request, from the same nine colours you use. | changes Joro |
sitemap_get |
Origins and paths seen in captured traffic, with methods and query parameter names. | |
websocket_list |
Captured WebSocket messages with direction, opcode and a text preview. Binary frames are reported by length only. |
Every read is bounded, and says so rather than trailing off. A capped table carries a note telling the client the offset to continue from, and a truncated byte range reports the total length alongside what it returned.
history_highlight is the one write here, and it is deliberately the weakest kind: it annotates, it does not report. An agent marking rows worth a second look is useful precisely because you see the colours while reading your own History — but a conclusion still belongs in findings_create or notes_create. Note that a highlight an agent sets appears on your next History load rather than live.
| Tool | What it does | Flags |
|---|---|---|
scope_get |
Reads the scope rules and whether scope is enabled. | |
scope_addrule |
Adds a single include rule. | changes Joro, scope-exempt only |
scope_enable |
Turns scope filtering on. Refused unless an include rule already exists. | changes Joro, scope-exempt only |
There is deliberately no exclude, remove or disable verb. Excluding or removing is the only direction that reduces what Joro observes, and on a scope-exempt token that is an evidence-suppression primitive: an agent could exclude its own targets, keep sending successfully, and leave you with no record of its traffic. scope_enable refuses on an empty rule set for the same reason — enabled scope with no rules captures nothing, and there would be no disable verb to undo it.
What you accept by granting these: scope is the interception decision, not a display filter. A new rule makes Joro terminate TLS for that host and record its plaintext, including your own browsing, and it is written into your project file. The read capabilities carry no target, so a host whitelist does not bound them — an agent can scope a host you left alone, wait for you to visit it, and read the result.
| Tool | What it does | Flags |
|---|---|---|
findings_list |
Passive detection findings as a table, with evidence redacted as the UI renders it. | |
findings_get |
One finding in full: rule, target, evidence offsets, notes, false-positive mark. | |
findings_create |
Records a finding the agent confirmed, keyed on host and title so re-reporting updates rather than duplicates. Appears in Detect as agent-reported. |
changes Joro |
findings_update |
Triage: mark or unmark a false positive, replace notes, override severity. | changes Joro |
notes_list |
Reads engagement notes, optionally for one host. | |
notes_hosts |
Lists the hosts that have notes. | |
notes_create |
Appends a note, attributed to agent: plus the token name. |
changes Joro |
These writes are additive on purpose. There is no notes_update, no notes_delete, and no findings_delete — an agent can add to your records and re-grade a finding, but it cannot remove what you wrote.
Note that findings_list leaves out Info severity unless it is asked for by name, which keeps a backlog of low-signal matches from crowding out the rest.
| Tool | What it does | Flags |
|---|---|---|
http_fingerprint |
Compact fingerprints of captured responses: status, length, timing, an exact body hash and a structural hash that ignores nonces and timestamps. | |
http_read |
A byte range of a captured request or response, reporting total length and offset so the client can page through it. | |
http_search |
String or regex search across captured traffic, returning match offsets and surrounding context rather than bodies. | |
http_diff |
Structured diff of two captured messages — status, headers and body reported separately, with volatile values ignored by default. | |
http_resend |
Applies structural edits to a captured request and sends it. | sends traffic |
http_batch |
Up to 50 labelled variants of one request, returned as a single comparison table. | sends traffic |
Sends go through Joro's own proxy. An agent's traffic is captured into History, scanned by Detect, entered in the Map, filtered by scope at both levels, and rewritten by Match & Replace and Custom Data — exactly like browser traffic. Expect the consequences:
- A Match & Replace rule may rewrite what the agent asked to send.
- An enabled request intercept parks an agent's send in your queue. The tool times out first and says that is what happened.
-
http_resendandhttp_batchare HTTP/1.1 only. A request captured over HTTP/2 is replayed as 1.1, because ALPN is pinned on the tunnel rather than driving the h2 path through it. - An upstream SOCKS proxy still applies, one hop later, through the proxy's own outbound dial.
- Redirects are not followed. A 302 to another host is a one-line scope bypass, so the agent reads
Locationand makes a second, separately checked call.
| Tool | What it does | Flags |
|---|---|---|
fuzzer_start |
Runs a wordlist against a FUZZ marker inserted by an edit, and returns a campaign id immediately. Capped well below the Fuzz tab: 500 entries and 64 KB of wordlist. |
sends traffic |
fuzzer_status |
Campaign progress: status, completed against total, error count. | |
fuzzer_results |
Results as a comparison table, with a computed grouping so outliers stand out. | |
fuzzer_stop |
Cancels a running campaign. Results already collected are kept. | changes Joro |
context_get |
Lists this token's session cookies by host. Values appear only with credential visibility on. | |
context_clear |
Drops this token's cookies, for one host or all. Your browser is untouched. | changes Joro |
The fuzzer differs from the send capabilities in one respect: it dials targets directly, the same way the Fuzz tab does, so its traffic is not captured — no history rows, no detect scan, no sequence numbers, and Match & Replace does not touch it. The metrics it returns are the whole record. It does still honor an upstream SOCKS proxy, and it still sends HTTP/2 as HTTP/2 where the captured request was h2.
Each token gets one cookie jar, which is how an agent works through an authenticated flow without ever being shown a cookie value. Jars are memory-only and are dropped on restart, on rotation, on disable, on revoke, and on a project switch.
| Tool | What it does | Flags |
|---|---|---|
config_intercept_get |
Reads whether request and response intercept are on, and how deep the queue is. | |
config_intercept_list |
Lists paused items with method, URL and how long they have been waiting. No raw bytes. | |
config_replace_list |
Lists Match & Replace rules. | |
config_replace_edit |
Adds or removes one Match & Replace rule. Patterns are compiled up front and rejected if invalid. | changes Joro |
config_customdata_list |
Lists Custom Data items. | |
config_customdata_edit |
Adds or removes one Custom Data item. | changes Joro |
config_noise_list |
Lists noise filter patterns. | |
config_noise_edit |
Adds or removes one noise filter pattern. | changes Joro |
Everything an agent adds here applies to your traffic too, not just its own, and is saved into your project. A Match & Replace rule rewrites bytes for all in-scope traffic; a Custom Data item is appended to every in-scope request. Where a change only needs to affect one request, a per-request http_resend edit is the better instrument, and the server tells the agent so.
config_noise_edit is the sharpest of these. The noise filter is checked before scope, and a noise match leaves nothing behind — no history row, no detection, nothing a rescan can recover. A pattern broad enough to cover a target silently discards its traffic.
Enabling or disabling these three features outright is yours alone, and so is forwarding or dropping an intercepted message. An agent can see that its send is parked; it cannot release it.
| Tool | What it does | Flags |
|---|---|---|
detect_rules_list |
Lists detection rules, built-in and yours, with enable state. | |
detect_rules_edit |
Adds a regex rule, removes one of its own, or toggles and re-grades any rule. | changes Joro |
detect_config_get |
Reads detection configuration: scan limits, skip lists, excluded hosts. | |
detect_config_set |
Patches detection configuration. Only the fields supplied change, but excludeHosts replaces the whole list. |
changes Joro |
detect_rescan |
Re-runs detection over already-captured traffic and returns as soon as the job starts. | changes Joro |
Rescanning is purely additive: findings dedupe by identity, your existing triage is preserved, and nothing is deleted. Rule edits land in your Detect tab live. Be aware that a disabled rule and an absent finding look identical from findings_list.
These five exist only when Joro was launched with --automation-privileged. No profile includes them, each has to be ticked deliberately, and every call is marked as privileged in Activity.
| Tool | What it does | Flags |
|---|---|---|
exec_webshell |
Runs a command on a host through an already-deployed web shell, using its auth key. | privileged, sends traffic, changes Joro |
c2_sliver_read |
Connection state plus Sliver sessions and beacons. Runs nothing on an implant. | privileged |
c2_sliver_command |
Issues a Sliver command in the same text form the Execute tab takes. | privileged, changes Joro |
c2_mythic_read |
Connection state plus Mythic callbacks. Tasks nothing. | privileged |
c2_mythic_command |
Issues a Mythic command. | privileged, changes Joro |
exec_webshell is held to the token's scope and host whitelist exactly as http_resend is. The four c2_ capabilities are not. Scope and a host whitelist describe web targets, not your own team server, so checking them would deny every call rather than bound anything — the grant is the only limit on what those reach. Joro says as much in the confirmation you have to clear to add one.
These three exist only under --automation-scripting, a separate flag from --automation-privileged. They are privileged on the same terms: no profile includes them, each is ticked by hand, and every call is marked privileged in Activity. See Scripting.
| Tool | What it does | Flags |
|---|---|---|
script_run |
Runs submitted JavaScript in a sandboxed worker process against Joro's SDK, and returns the logs, the return value and how it ended. | privileged, changes Joro |
script_list |
Lists the automations installed on this Joro: id, version, whether you enabled them, what they are armed for, how the last run ended. Never returns source. | privileged |
script_invoke |
Runs an automation you installed and enabled, by id, with input of the agent's choosing. | privileged, changes Joro |
script_run is the one capability whose authority is not its own. Its run is authorized with a fixed SDK bundle rather than the granting token's capability list, so a token holding only instance_get and script_run can run code that reads history, resends requests and writes findings. Grant it on that understanding. What the token's own settings still control is reach, not permission: the run inherits its host whitelist verbatim, has scope enforcement pinned on, and has credential visibility pinned off, whatever the token says.
It exists because an agent sweeping fifty object IDs otherwise spends fifty round trips of model context doing one http_resend at a time.
script_list plus script_invoke is the narrower posture, and usually the right one. It lets an agent run automations you wrote and reviewed without letting it submit code. An agent may invoke only an enabled automation; you can always run a disabled one yourself, because reviewing something means running it before arming it.
A profile fills in a grant list and the token shape it expects. Pick one, then edit freely. Each is built as one job's worth of access, which makes a profile the natural unit for a single-purpose agent — see Automation-Examples.
| Profile | Grants | Scope required | Sends | Credentials | Limits |
|---|---|---|---|---|---|
| Recon (read-only) | 14 | yes | no | no | 60/min · 2 |
| Active tester | 26 | yes | yes | no | 120/min · 4 |
| Triage analyst | 19 | yes | no | no | 120/min · 2 |
| Engagement setup | 16 | no | no | no | 60/min · 2 |
| Full operator | 41 | no | yes | yes | 120/min · 4 |
A profile is expanded at creation and is not remembered on the token. This is the no-wildcards rule one level up: a capability added to a profile in a later release must not retroactively widen a token you issued today. The new capability is surfaced against the token instead.
Two profiles are shaped by constraints worth knowing:
- Engagement setup ships the scope writes, so it has to run with the scope requirement off. Because a scope-exempt token's reads are bounded by nothing, it is the one profile that is not built on the read-only base — it is configuration-only and reads no captured traffic at all. It cannot list hosts, so name the ones you want scoped.
- Full operator is every capability except the privileged ones, with scope enforcement off so its scope grants work. That means it can reach any host. Issue it for an agent you are watching. Because it is built from the registry rather than a fixed list, a launch flag never widens it: execution, C2 and scripting stay out of it whether or not their flags are set.
A send is allowed only if the scope requirement is off, or scope is enabled with at least one rule and the target matches — and the host whitelist is either empty or matches.
This fails closed deliberately. Scope is a capture filter rather than an authorization control, and it reports "in scope" when it is disabled, so consulting it alone would let an agent reach any host in exactly the state a fresh install is in. The whitelist ANDs; it can narrow what a token reaches but never widen it.
Host globs match the way the rest of Joro's host patterns do, which means * does not stop at a dot: *.target.com matches a.b.target.com but not bare target.com. Write both when you mean both.
If a token that can send is active while scope is off or has no rules, the page says so and its sends are being refused. Either add an include rule under Settings → Project → Filtering, or reissue the token with the scope requirement off.
With Show credential header values off, the values of Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key, X-Auth-Token, X-Csrf-Token and X-Xsrf-Token are masked wherever bytes are returned:
- Masking is length-preserving, so
http_read's offsets stay aligned withhttp_searchand the History Raw tab. - Masked output always carries a
redacted:line naming what was hidden. A silent mask reads as an absent header, and an agent would report an authenticated endpoint as unauthenticated. -
http_searchmasks before matching, so it cannot be turned into an extraction oracle. -
http_diffredacts these regardless of the setting. It reports presence and change, which is all a diff needs, and a diff is exactly where a live session token would otherwise be copied into a model's context.
The setting defaults off, which narrows any token issued before it existed. Note one residual: history_list's content filter matches the bytes as captured, so a token without credential visibility can still confirm a guessed value by row count. That is a confirmation oracle against a rate-limited, audited token, not a way to extract one, and the tool's own description says so.
Activity at the bottom of the page is one entry per invocation attempt, newest first, with Denied and Errors filters. Because entries are written as a call unwinds, refusals, timeouts and recovered panics all land — including an agent probing for a tool it was never granted, which is the case you most want to see.
Each row carries the token, the capability, the result, output size and duration, plus markers for a privileged call and for one that could return credential values. For a mutating call it shows change, the handler's own description of what it altered; for a send it shows method, host and path.
Arguments are not recorded, only a digest. A send's arguments carry cookies, credentials and payloads. The change string is the deliberate exception: a scope rule or a Match & Replace pattern is configuration rather than a secret, and without it you could see that an agent edited your proxy but not what it did.
It is called Activity and not an audit log on purpose. It is an in-memory ring of 2000 entries, unsigned, and gone when the process exits — useful to you, and no use as evidence to anyone else. Clear empties it.
The same data drives the Automation Activity widget on the Dashboard.
Beneath it, Script runs logs sandboxed JavaScript runs — one row per run, with the source it ran kept verbatim. See Scripting.
Issuing tokens, editing grants and controlling the MCP listener can never be capabilities. Those names are reserved, registering one fails outright, a hand-edited file has them stripped on load, and the MCP listener runs on its own mux where the main API's routes do not exist at all. There is no path from a bearer token to token administration.
Beyond that, some omissions are deliberate rather than pending: excluding, removing or disabling scope rules; forwarding or dropping an intercepted message; turning Match & Replace, Custom Data or the noise filter on and off; sending a WebSocket frame; deleting a note or a finding; purging findings on a rescan.
Finally, the honest framing: capability tokens are a leash on the agent, not a sandbox on the machine. Joro's local API already admits any non-browser client running as you, and any such process can read ~/.joro/automation.json. What a token buys you is scoping, a record, and a kill switch for the agent you chose to run.
All three are set at launch and cannot be changed from the UI, so none can come on without a restart. See Configuration.
-
--no-automationremoves the feature entirely: no routes, no token file, no second port. The page then shows only that automation is disabled for this run. -
--automation-privilegedmakes the execution and C2 capabilities grantable. Without it they cannot be listed, granted or invoked, and a grant stored from a previous run is inert. -
--automation-scriptingdoes the same for the three script capabilities. See Scripting.
The last two are separate axes on purpose: web shell execution and C2 are one decision, and handing an agent a scripting sandbox is another. Take either without the other.
- Automation-Examples — worked examples: a read-only recon agent and a scope-setup agent, with the token and client files for each
-
Scripting — the JavaScript sandbox behind
script_run, and the automations you can install and arm yourself - Settings — the Automation category, and the scope and filtering rules an agent's sends are checked against
- History — where an agent's sends land, alongside your own traffic, and the highlights it can set
- Detect — findings an agent records or re-grades, and the rules it can edit
- Projects — automation tokens are deliberately not part of a project file
- Configuration — the two launch flags