Hunt

Work with an agent (Claude Code, Codex, OpenCode)

Two commands on a laptop, connect and agent, give you the same ShakerScan agent workspace the open-source launcher provides, against your Enterprise instance, with every action under your identity.

SectionHunt
UpdatedNovember 9, 2026
EditionSelf-hosted Enterprise, public beta

ShakerScan is built for an engineer working with an AI agent. The open-source launcher's shakerscan agent claude starts the agent inside a workspace that already knows ShakerScan: project commands such as /scan, /findings, /status and /deep-hunt, the skills, and the operating rules. The shakerscan client gives you exactly that against your Enterprise instance, from a laptop, without Docker and without the engine:

pipx install shakerscan                                                 # once
shakerscan connect https://scanner.example.com/_enterprise/connect/<code>  # the line from the console
shakerscan agent claude                                                 # or codex, opencode, pi

Inside the session the agent starts scans, reads findings, drives Hunts and checks status the same way it does against a local engine. Every call goes through the gateway under your service token's identity and role and lands in the audit history. The agent's own model does the reasoning; your instance's model credentials are not involved.

One command

  1. An administrator opens the Enterprise console, API access, and creates a token for a service account with the operator role, a short lifetime, and a label naming the person.

  2. The console shows one command with a Copy button. Paste it on the laptop:

    shakerscan connect https://scanner.example.com/_enterprise/connect/<code> --claude
    

That is the whole setup. The link in the command works once and expires after ten minutes, and the command carries no secret: the client fetches the token through the link, saves it to ~/.config/shakerscan/token (readable only by you) with the instance address beside it, checks the connection, and, with --claude, registers ShakerScan in Claude Code. From then on shakerscan mcp, shakerscan hunt … and shakerscan doctor need no options.

You need Python 3.10 or newer on the laptop and the client itself: pipx install shakerscan (or uv tool install shakerscan, or brew install andriyze/shakerscan/shakerscan). If the open-source engine is also installed on this laptop with the curl one-liner, the same command name is already there; that is deliberate, the client behaves identically and hands engine commands to the local install. If pipx refuses to overwrite it, pipx install --force shakerscan.

The agent session

shakerscan agent prepares a workspace (~/.local/share/shakerscan/agent, or the current directory with --here) with the ShakerScan operating rules, skills and project commands, a note naming the connected instance, and the MCP server registered for that workspace; then it starts the agent there. Two things differ from a local session, and the note tells the agent so:

  • There is no local engine, so nothing starts or stops from the workspace; the instance is already running.
  • A route the beta keeps closed answers with a refusal that names what is missing (the role, Hunt or network scans being off, or the engine feature by name); the agent reports it and takes another path. A route the gateway does not know, for example one that does not exist on the engine, answers 403 "not available through this gateway (unknown or unsupported route)": that is not a feature switched off, so check the path rather than asking to enable something.

Underneath, the kit's API calls go through shakerscan api METHOD PATH [JSON], which you can use yourself (shakerscan api GET "/findings?limit=20"); shakerscan scan … submits and follows scans, shakerscan hunt … drives Hunts, and shakerscan doctor reports the instance.

Check the connection

connect runs this for you; run it again any time:

shakerscan doctor

A healthy answer names the client version and the saved instance, confirms the token is set (sent as a bearer token, over HTTPS only), reports the engine reachable, and lists the tool catalogue the gateway exposes: read-only Arsenal tools plus Hunt tools (30 on engine 2.11: 9 read-only Arsenal tools and 21 Hunt tools; doctor prints the count your instance offers).

doctor or connect saysMeaningWhat to do
this connect link has expired or was already usedLinks live ten minutes and work once.Create a new token in the console and paste its command.
Authenticated remote APIs require HTTPSThe address is plain http://. A token is never sent over HTTP.Use the HTTPS address.
ShakerScan API is unavailable: <reason>The laptop could not reach the instance; the reason is the network error (timed out, refused, certificate).Check the address, VPN, firewall. A per-application firewall may need to allow the new command once.
cannot reach the instance: [SSL: CERTIFICATE_VERIFY_FAILED] ... (or the same in doctor)The instance's certificate comes from a CA this laptop does not trust, such as your organization's own CA.Trust that CA for the client: see A certificate from your own CA.
ShakerScan API returned HTTP 401The token is wrong, expired or revoked.Ask an administrator for a new token.
An empty tool listThe gateway is older than 0.2.0-beta.5.Upgrade the instance.

A certificate from your own CA

When the instance uses a certificate from your organization's own (private or corporate) CA, the client refuses it until the laptop trusts that CA: connect and doctor answer [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain. The client (0.11) uses Python's standard certificate checks, which read the SSL_CERT_FILE environment variable; it has no option or profile setting of its own for a CA yet. SSL_CERT_FILE replaces the default list of trusted CAs, so point it at a file holding the usual public CAs and yours:

mkdir -p ~/.config/shakerscan
cat "$(python3 -c 'import ssl; print(ssl.get_default_verify_paths().openssl_cafile)')" \
    corporate-root-ca.pem > ~/.config/shakerscan/ca-bundle.pem   # or your system's CA bundle
export SSL_CERT_FILE=~/.config/shakerscan/ca-bundle.pem          # in your shell profile
shakerscan connect https://scanner.example.com/_enterprise/connect/<code>

The variable must be set for every shakerscan command, and for the MCP server your agent starts: shakerscan agent passes it on to the agent it starts, but an agent you registered by hand starts shakerscan mcp with its own environment, so give it there too (claude mcp add --scope user -e SSL_CERT_FILE=$HOME/.config/shakerscan/ca-bundle.pem shakerscan -- shakerscan mcp, or an "env" entry beside "command" in the JSON below). If your company installs its CA into the operating system's store, a Python whose default bundle is that store needs nothing more.

Register the MCP server in an agent

connect --claude registers Claude Code for you. To do it by hand, or for another agent:

claude mcp add --scope user shakerscan -- shakerscan mcp

Any MCP client that spawns a stdio server, in its configuration (the saved profile supplies the instance and token):

{
  "mcpServers": {
    "shakerscan": {"command": "shakerscan", "args": ["mcp"]}
  }
}

On a machine without a saved profile, a CI runner for example, pass the connection explicitly with --url and the SHAKERSCAN_API_TOKEN_FILE environment variable.

The agent now sees the Hunt tools (shakerscan_hunt_start, shakerscan_hunt_get, shakerscan_hunt_query, skills, capabilities, candidates, verification, finish and cancel) and the read-only inspection tools (targets, findings, evidence manifest, timeline, plans, tool status, attack-surface gaps). The attack-surface gaps tool answers that the feature is not enabled until an administrator turns on exposure and attack-surface monitoring. Every Hunt is bound to a target your instance has authorized, and the engine's own scope, approval, budget, evidence and proof rules apply unchanged.

Scripted Hunts

The same client carries the Hunt command line, using the saved instance:

shakerscan hunt list
shakerscan hunt start --help

Connection options come first; everything after them is the Hunt subcommand (start, get, list, query, call, candidate, verify, finish, cancel, resume).

Approve Hunt requests in your terminal

An agent's Hunt sometimes needs a decision only a person may make: raising its budget, using another target's saved credential, reaching another host or port, or a state-changing or active-testing step its policy does not include. The Hunt then waits with a permission request, and the agent names its id. The service token never approves on its own: you decide in your own terminal and prove it is you.

shakerscan approve <request-id>          # or: shakerscan deny <request-id>
shakerscan approve --all-pending         # every pending request of your open Hunts (--hunt ID: one Hunt)
shakerscan approve --watch               # stay open and decide each new request on a keypress

The approver is the person your connection's token acts for: client 0.11.2 with gateway 0.2.0-beta.47 asks no sign-in name (name another operator with --account, which a token that acts for nobody always needs). Against an earlier gateway, client 0.11.2 asks for your sign-in name once, before it begins, as client 0.11.1 always does. The proof (--method) is a USB/NFC security key (security_key; client 0.11.2 installs python-fido2 with itself; 0.11.1 needs pipx inject shakerscan fido2) or the code from your authenticator app (totp). Without --method, client 0.11.2 uses your security key whenever one that holds one of your keys at this gateway is plugged in (it asks the key silently first, so nothing blinks): one touch, no PIN, with the switch below on or off. A knowledge or secrets decision from the terminal (shakerscan knowledge ..., shakerscan secrets reveal|grant|promote) asks the key's PIN as well while the switch is off, as on earlier gateways. The authenticator code only when no key can answer (none registered, or none plugged in); --method totp chooses the code anyway. Client 0.11.1 asks for the code whenever you have one, and for a touch and the key's PIN with --method security_key. A Homebrew install of the client has no security-key support, so install it with pipx or uv tool for the key. With --watch, one proof opens a 30-minute approver session that only that process holds (--minutes for less); Ctrl-C ends it. When an administrator requires a security-key touch (below), each decision is one touch instead, and there is no approver session. The commands ask only in your own terminal: client 0.11.2 asks when its input and its output are a terminal, writes its prompts there, and otherwise exits with status 3 having asked nothing of you or your key, so a command the agent runs in its own shell, or one whose output a program captures, cannot decide anything or make your key blink. The approver must be an operator or administrator, the token an operator token, and every decision is recorded with the person who made it.

Single sign-on people prove it in the browser instead: --method sso (client 0.11.1 also needs --account sso:<subject>, where the subject is your identity provider's id for you; My account shows it after "Signed in as") prints a link on this gateway where you review the exact decision and sign in again at your identity provider, then press Enter in the terminal. A security key registered under My account answers with a touch instead, with the touch switch (below) on or off (with it off the browser stays the fallback): it works until an administrator disables your account here, and a disable at your identity provider reaches this gateway only at your next sign-in. Only a token whose Acts for field names you can start this, so an administrator creating your token in the console chooses you under Acts for (it is optional otherwise: a shared or automation token acts for nobody). While single sign-on is required on the instance, the break-glass administrator is the only local account that can approve from a terminal.

Pre-authorize an agent's work when you start it, so its Hunts do not stop for what you already allow:

shakerscan agent claude --allow budget.raise:2x --allow capability:state-changing

Bounds are budget.raise:<factor>, credential.use:<targets>, target.authorize:<patterns>, capability:state-changing and capability:active-testing (a target.authorize bound that names a Hunt's own target by its exact host also authorizes that target; see below). The client asks for your proof once, before the agent starts (the same --method), and the bounds hold for that agent's Hunts until the token's working day ends (at most 12 hours, never past the token). The gateway records them as yours and never widens them; anything outside them is still a request for you to approve.

Decisions only a person should make

The gateway cannot tell your terminal from your agent: shakerscan agent gives the agent the same token file your terminal uses. Some requests record a decision the product reserves for a person:

  • raising a running Hunt's budget (POST /hunts/{id}/budget-amendments, also with operator_confirmed: true);
  • authorizing a target for active testing (POST /targets/{id}/authorization, a target added with its authorization, or the authorization the gateway records when an active scan or Hunt starts on a target nobody authorized yet), sharing a saved credential with another target, and writing scope or approval receipts;
  • widening a target's Hunt permissions (shared credentials or collections, SSH host keys, first-contact SSH trust); narrowing them is never a person's decision;
  • confirming a device SSH plan, approving an authorization investigation, confirming that you may probe a connected device's service, moving a device to a new address, acknowledging a device credential's lockout and choosing a device's automation policy;
  • reactivating a discovered secret or changing its expiry (revoking one is not).

By default a service token may make them, and so may an agent that uses your token. That is an accepted risk, chosen so that nothing an agent or script already does breaks: with the switch off, these requests work exactly as on gateways before 0.2.0-beta.47. Every such decision made with a token is written to the audit log as person_decision.token, outcome unproven (made with a token, not proven by a person), with the route, the decision, the token and the person it acts for, never the request's body; shakerscan approve --watch (client 0.11.2) prints a line for each one made through any of your connections.

An administrator can close this gap with one switch, described next, with a YubiKey for each person. Try a touch with your own key from your own terminal first: shakerscan approve --test (client 0.11.2; Try a touch) asks one touch, which the gateway checks as it checks every touch, decides nothing and works with the switch off; turn the switch on for everyone once that works on the computers your people decide from.

Require a security-key touch for person decisions from terminals

In short. With this switch on, each decision above that comes from a terminal takes one touch of a security key registered to the person, in that person's own terminal; an agent using the person's token is refused and told the command the person runs. With it off (the default), nothing is refused: an agent using your token can make these decisions, and each one is in the audit log. We recommend a YubiKey for each person. Browser sessions are unchanged either way. The details (summaries, what the gateway writes, limits, keys, where the key is, client 0.11.1, the protocol) are in Security-key touch: reference.

Turn it on (administrator)

  1. Have each operator and administrator register a YubiKey (next) and install shakerscan client 0.11.2 or later with pipx or uv tool (a Homebrew client cannot touch).
  2. Have each of them try a touch: shakerscan approve --test (below).
  3. In the console, Settings, Scanning, tick Require a security-key touch for person decisions from terminals and save; the console says who has no key yet and asks you to confirm.
  4. Ask people to start their agents again with shakerscan agent --allow ... (start bounds pre-authorized with a code before are refused at the agent's next Hunt start).

Only an administrator's browser session changes it (audited as settings.person_decision_proof; in the gateway's backup); not while the gateway is reached by an IP address. People on Windows, or on Linux other than 64-bit x86 or ARM, decide in the browser. Turn it off again before you roll the gateway back to a release before 0.2.0-beta.47 (Before a rollback, below).

Register a YubiKey (each person, once, in the browser)

  • A local account. My account, Passkeys: name the key, choose Use it: for terminal approvals (touch; never signs you in), type your current password, click Add a passkey, touch the key (choose "security key" if the browser offers iCloud Keychain or a phone). It never signs you in and needs no PIN with client 0.11.2. A sign-in passkey on a USB key answers too; one saved on this computer, a phone or by Touch ID never answers a terminal.
  • A single sign-on account. My account, Security key for terminal decisions, Sign in again to add or remove a key (your identity provider asks you to sign in again in its window), then Add a security key. It works until an administrator disables your account here; a disable at your identity provider reaches this gateway at your next sign-in. Use client 0.11.2; with 0.11.1 and the switch off, keep running shakerscan approve --method sso; with the switch on, 0.11.1 takes only --method security_key with a key that has a PIN, so upgrade.

Try a touch

$ shakerscan approve --test
Touch your security key to test it; this decides nothing, as alice (Ctrl-C to cancel)...
Your security key answered for alice: a touch from this terminal works. Nothing was decided.

It is checked as every touch is, with the switch on or off. shakerscan doctor says whether the switch is on and whether a key is plugged in.

What a touch looks like

$ shakerscan hunt budget raise 3f2a91c0 --limit max_state_changing_requests=40
This needs your decision (your administrator requires a security-key touch):
  Raise state-changing requests 20 -> 40 (safety ceiling) for Hunt 3f2a91c0… on honey.example.com
Touch your security key to approve (Ctrl-C to cancel)...
touch accepted for alice (security-key touch); sending the request again...
raised state-changing requests 20 -> 40 (safety ceiling) for Hunt 3f2a91c0-…

Read the decision it names, then touch; Ctrl-C cancels and nothing is changed. With no key plugged in, the client says "Insert your security key" and waits up to a minute. The same command in an agent's shell never prompts and never makes your key blink: it prints one line naming the decision and your command, says nothing was changed, and exits with status 3. No option skips the touch.

What changes when it is on

  • A service token's person decision is refused (HTTP 403, person_decision_required) unless it carries a one-time proof from a touch of a key registered to the person the token acts for: that exact request, once, for two minutes, named in plain words first ("Authorize staging.example.com for active testing in the production environment").
  • Every terminal approval is one touch too (approve, --all-pending with one touch for every request shown, agent --allow, knowledge and secrets decisions), a deny as well as an allow: no code, browser hand-off or approver session decides, because an agent can type those.
  • Revealing, granting or promoting a discovered secret in the browser takes one touch of a USB or NFC key registered to the person too, in the dialog the scanner page opens; no code, sign-in or passkey saved on the device decides it.
  • A scan, Hunt start or retest a token sends runs on a target already authorized; on one nobody authorized, the engine's refusal becomes the person's decision (shakerscan targets authorize <target> for you).
  • The Hunts one agent session starts share one safety budget, which the touch at shakerscan agent --allow approves (the command shows it). A start past what is left asks your touch (the agent then finds the Hunt you started); a raise past it becomes the Hunt's own request for shakerscan approve. A connection without an agent session has one balanced Hunt's safety ceilings per 12 hours.
  • Confirming a device SSH plan and approving an authorization investigation are made in the browser only. A token that acts for nobody (a shared or automation token) cannot carry these decisions; a person makes them in the browser.

What the switch protects, and what it does not

  • It protects the decisions above, and within an agent session cancelling a Hunt and starting another no longer resets anything (the session's safety budget). It bounds the safety ceilings, not a Hunt's requests, calls or duration, and a target's authorization still covers every Hunt and scan of it.
  • A touch proves presence, not identity, and not what the person saw: a key has no display. A program running as your own computer user can rewrite the client, or start a request of its own the moment you expect to touch. Touch only for a prompt you started, in your own terminal. Physical access to a plugged-in key is a presence proof, not an identity proof.
  • An agent's tool call has no terminal and gets no prompt, and a command whose output is captured (| tee, $(...), > file) is never asked for this touch. An agent started some other way than shakerscan agent whose commands run in a full pseudo-terminal (script, expect, tmux) does get it: start agents with shakerscan agent, or as another computer user or in a container with its own connection.
  • Decisions tokens made while it was off stay in force: review the person_decision.token rows in the audit log and revoke what no person made.
  • The key must be on the computer that runs the command; over SSH, run the same command on your laptop with its own connection (and preview an approval's scope again through it), as the reference says.

Before a rollback

Gateways before 0.2.0-beta.47 read the switch as off (person decisions from terminals are then unprotected) and refuse every console save while it is saved. Turn it off first. If you did not, remove it on the host, in the release directory:

docker compose -f compose.json exec -T enterprise python -c 'from shakerscan_saas.enterprise.settings import Settings; from shakerscan_saas.enterprise.store import Store; s = Settings(); Store(s.state_dir, s.installation_id, mfa_key_file=s.mfa_key_file).save_settings({"person_decision_proof": None}, set(), "operator-cli"); print("removed")'

Console saves work again at once (audited as settings.change).

Making these decisions yourself

  • Raise a Hunt's budget. When a limit runs out, the Hunt raises a budget.raise request: approve it with shakerscan approve <request-id> (the Hunt then continues), allow it up front with shakerscan agent --allow budget.raise:2x, raise it yourself with shakerscan hunt budget raise <hunt> --limit <name>=<N> (client 0.11.2), or raise it on the Hunt page in the browser. An agent asked to "raise the budget" names the request id to you instead.
  • Authorize a new target. shakerscan targets authorize <url> (client 0.11.2; also a target's or connected device's id, or host://<address> for a host target), or once in the browser: add it on the Targets page with authorization ticked, or start a scan or Hunt of it there. With the switch on, --allow target.authorize:<host> with the target's exact host name records the authorization in your name when one of an agent's Hunts starts with active testing on that host, unless the target is stored in a lab environment (then the start asks for your touch, which names it). That bound covers the Hunts the agent starts only: an agent's own scan or retest of a host no Hunt has authorized yet is refused and asks for a touch of its own. With the switch on, authorize the agent's targets before you start it (shakerscan targets authorize <url>, one touch each): a target's standing authorization covers every later scan, retest and Hunt of it. A wildcard (*.example.com) lets a running Hunt reach other hosts but never authorizes a Hunt's own target, and neither does an IP address. With the switch off, a token's Hunt start authorizes its target in the token's name, as before.
  • Everything else on the list: in the browser, or with shakerscan api <METHOD> <path> '<json>' from your own terminal, which asks for the touch the same way.

Rules worth telling every token holder

  • One token per person, operator role only when Hunt is needed, viewer otherwise. Tokens expire within 90 days and can be revoked at any time in the console; disabling the identity revokes them immediately.
  • The token lives in a file only you can read. It is sent only over HTTPS and never printed by the client. shakerscan disconnect deletes it when you are done with an instance.
  • A service token cannot open the Enterprise console or sign out a browser session; it is for the client only.

Not in this beta

Running Arsenal commands with execute=true through the client is refused by name; the read-only commands run as bounded reads. The autonomous Hunt, where the instance reasons with a model you configure, is a separate administrator switch and does not need a laptop client.