Skip to content

Automation

Tony West edited this page Aug 18, 2026 · 6 revisions

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 SettingsAutomation, and applies to proxy mode only.

Enabling the MCP server

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.

Connecting a client

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 Host header must be a loopback name, with no --allowed-host escape. Any Origin header at all is a 403, as is a Sec-Fetch-Site other than none — 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 /mcp returns 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.list is history_list, config.noise.edit is config_noise_edit. There is no joro_ prefix on tool names — the client already namespaces by server, and every name sits in the model's context on every turn.
  • tools/list is 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.

Tokens

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.

The token table

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, Cookie and 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.

Where tokens are stored

~/.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.

Capabilities and grants

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-privileged for execution and C2, --automation-scripting for the script capabilities — and is in no profile.
  • scope-exempt only: refused unless the token has the scope requirement off and no host whitelist.

Orientation and captured traffic

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.

Scope

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.

Findings and notes

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.

HTTP tools

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_resend and http_batch are 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 Location and makes a second, separately checked call.

Fuzzer and session context

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.

Proxy configuration

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.

Detection

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.

Command execution and C2

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.

Scripting

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.

Profiles

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.

Scope, host whitelist, and credentials

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 SettingsProjectFiltering, 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 with http_search and 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_search masks before matching, so it cannot be turned into an extraction oracle.
  • http_diff redacts 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

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.

What automation cannot do

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.

Launch flags

All three are set at launch and cannot be changed from the UI, so none can come on without a restart. See Configuration.

  • --no-automation removes the feature entirely: no routes, no token file, no second port. The page then shows only that automation is disabled for this run.
  • --automation-privileged makes 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-scripting does 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.

Related

  • 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

Clone this wiki locally