-
Notifications
You must be signed in to change notification settings - Fork 2
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.
Streamer mode bars token names, the joro_ prefix display, the MCP endpoint, and the one-time secret and copy-config blob shown after issuing or rotating. Copy still works on a barred value, which is the case that matters here: a one-time secret is shown once and never again, so you can put it on your clipboard while it is barred, but you cannot read it off the screen. See Settings.
~/.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 53 capabilities, or 43 with neither launch flag: --automation-privileged adds five, --automation-scripting adds five. Sixteen of the forty-three 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.
Three conventions hold across the catalog rather than per tool.
A captured request is named by seq. Every capability that takes one uses seq, and a list of them seqs — the sequence number History shows. The older name ref is no longer accepted: it was renamed, not aliased, so a client still sending ref gets an error rather than a silent misread. Affected: http_read, http_search, http_diff, http_resend, http_batch, history_highlight and fuzzer_start.
host on a listing is a case-insensitive substring. acme matches app.acme.com, and a full FQDN works too, with or without a port. The exception is detect_rescan, whose host is matched exactly, so pass a whole host as history_stats reports it.
An empty listing says which kind of empty it is, rather than returning nothing and leaving a mistyped host to read as an empty capture store. It distinguishes nothing captured yet, a host that matched with other filters emptying the result, and a host that matches nothing captured — naming up to ten hosts that do exist, with history_stats for the rest.
An unknown argument is answered with a suggestion when one is close enough:
unknown field "ref" — did you mean "seq"?
Matching ignores case and separators, so max_matches and MaxMatches both resolve to maxMatches. Two equally close candidates produce no suggestion rather than a wrong one, and the accepted names are not listed back — every capability already publishes a complete input schema.
| 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 |
notes_delete |
Withdraws a note the agent itself filed, by ID. | changes Joro |
findings_delete |
Deletes a finding the agent itself reported, or clears every finding marked a false positive. | changes Joro |
Deletion is bounded rather than absent. An agent can withdraw its own records and clear noise you have already dismissed; it cannot remove what you wrote.
-
notes_deleterefuses any note not attributed to the calling token, and authorship cannot be spoofed — it is read from the stored note, not from the caller.notes_listshows who filed each one. -
findings_deletetakes either one finding the agent reported throughfindings_create, or — withfalsePositives— every finding currently marked a false positive, whoever reported it. A live scanner finding is refused, with instructions to mark it withfindings_updatefirst. Deletion leaves no record, so the mark is the better move when the conclusion is worth keeping.
There is still no notes_update: a note is withdrawn and refiled rather than edited in place.
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.
| Tool | What it does | Flags |
|---|---|---|
webhook_list |
Lists the endpoints you opened to automation, by id and name. Never returns a destination. | |
webhook_fire |
Sends a notification through one of them, named by id. | changes Joro |
These are not privileged and need no automation flag — only --no-webhooks removes them. They sit between detection and execution in the grant picker rather than among the reads, because this is the one class whose bytes leave the engagement entirely.
An agent chooses among your endpoints and never chooses where. The URL, the headers and any credential are yours; the body is your template, and the agent's message fills the one slot reserved for it. An endpoint you did not tick Let automations fire this on cannot be fired and cannot be told apart from one that does not exist — the refusal is worded identically, so a script cannot enumerate what you have configured.
Neither scope nor the token's host whitelist applies. Both describe web targets, and this is your own notification channel. Firing is rate limited to 10 per minute per principal, so a long-lived token shares one budget across every call it makes. Activity records the webhook's id and the status, never the payload. See Webhooks.
These are remote execution capabilities: a command run on a target host through a web shell Joro deployed there, and commands issued to your C2 server. For running a command on your own machine, see Scripting#local-commands — a separate feature, not a capability, and no token can reach it.
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 five 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_install |
Stores a script as an installed automation, so it outlives the session and you can read, edit, arm or remove it. | privileged, changes Joro |
script_replace |
Replaces the code of an automation already stored here, by id. | 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 the token's host whitelist, scope requirement and credential setting verbatim.
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.
script_invoke never runs a command automation, whatever it holds. See Scripting#local-commands.
script_install adds durability, not execution authority. What it writes arrives disabled: the triggers it declares are a request rather than an arming, script_invoke refuses it until you enable it, and only you can change that.
An agent declares bare trigger names and nothing more. It picks from manual, request.selected, detect.finding, fuzzer.complete, automation.completed and request.captured; it cannot attach conditions to any of them. The filters that narrow an event are yours, written in Settings → Automation → Scripting, and a package installed this way lands disabled anyway — there is nothing an agent could usefully filter before you have read it. It also cannot submit a graph: the canvas is your working document, and a caller supplying one would be submitting a second description of code it is already submitting. The two could disagree, and the .js is what runs. See Scripting#visual-scripting. An id that already exists is refused rather than overwritten, the source is compiled before it is stored so a syntax error never lands on disk, and no agent can read any installed automation's source back — including its own. An agent may hold at most 32 packages of its own. It cannot install a command automation.
script_replace is a separate grant, so you can permit storing something new without permitting rewrites. It works only on an automation you do not currently have enabled, and requires the sourceHash from script_list — a stale hash is refused rather than clobbering a revision the agent never saw. The replacement stays disabled. Note how wide the rest of it is: any disabled automation can be replaced, including one you wrote yourself and left switched off. If the automation was built on the canvas, replacing its source drops the canvas with it — the manifest an agent submits carries no graph, and what remains is the code.
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 | 28 | yes | yes | no | 120/min · 4 |
| Triage analyst | 21 | yes | no | no | 120/min · 2 |
| Engagement setup | 16 | no | no | no | 60/min · 2 |
| Full operator | 45 | no | yes | yes | 120/min · 4 |
Triage analyst also clears away its own entries and dismissed noise, so it carries findings_delete and notes_delete alongside the writes.
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, and — since the webhook capabilities are not privileged — that it can fire any endpoint you opened to automation. 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. It is also the one profile a new capability class grows on its own, which is why the count moved when webhooks arrived.
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 names what was hidden: a tool gets a
redacted:line, a script gets aredactedarray. A silent mask reads as an absent header, and an agent would report an authenticated endpoint as unauthenticated. - Either names only the withheld values inside the bytes that call returned. A response-only read never names a header the request carried, and a body read names nothing at all.
-
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 filed by anyone else; deleting a live finding you have not dismissed; editing a note in place; 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 five 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 five script capabilities. See Scripting. -
--automation-commandsallows installed command automations to run a local operating-system command, on a trigger or as a lens. It grants nothing to a token: no capability sits behind it, and it is reachable only from automations you installed yourself. See Scripting#local-commands. -
--no-webhooksremoves outbound webhooks, and with themwebhook_listandwebhook_fire. It is the odd one out here: webhooks are on by default and need none of the automation flags, so this is the only flag in the list that takes something away rather than offering it. See Webhooks.
The middle three are separate axes on purpose: web shell execution and C2 are one decision, handing an agent a scripting sandbox is another, and letting your own automations run local tools is a third. Take any without the others.
- 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 script and command 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 five launch flags