Trace review showed the split rule firing on commands with no execution at all:
curl downloading a `.js` asset (`curl …/app.js > app.js`), glob and grep
patterns containing a script suffix, and running an inspectable workspace script
with an output redirect (`python3 probe.py > out.jsonl`). It blocked on any
interpreter segment or any token ending in a script suffix.
Block only the shapes whose executed code no artifact can describe: an
interpreter that reads from a pipe, stdin, or heredoc, and create-then-run where
a script-suffixed file is written (`> x.py`, `-o x.py`) and executed in the same
expression. Running an inspectable script with its output redirected or piped,
and downloading a script-named asset, now go to review — the script itself is
still read into the packet. Destructive-in-a-chain detection is unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Recon tools route their target list through a flag — `ffuf -w wordlist.txt`,
`httpx -l hosts.txt`, `nuclei --list targets.txt` — not the `<` redirect the
input-file collector already handled, so the reviewer kept blocking "probes
every host listed in hosts.txt" because the list was never in the packet. Parse
the value of the common list-file flags and collect it the same bounded way,
alongside redirect inputs. The value is only read when it resolves to a
workspace file, so a boolean `-l` (grep, wc) whose next token is not a file
collects nothing and never makes the packet incomplete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Trace review of two guarded-mode runs found the unresolved-execution guard
firing on ordinary commands: curl fetching a .js asset, rg over a .py file, sed
and cat and cp on script-named files — 24 blocks in one run. The guard was meant
to fail closed on an unknown interpreter handed a script, but a read, transfer,
or text tool takes such a file as data, not as a program to run.
Exclude known read commands, HTTP clients, and a set of text/data tools from the
script-suffix branch, so only a genuinely unknown executable given a script still
fails closed. awk moves from the interpreter set to the data tools: its program
is an inline positional argument, not a -c or script file the entrypoint reader
can resolve.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two guarded-mode false-positives from a recon run.
A command that reads a workspace data file — `while read host; do dig "$host";
done < hosts_passive.txt` — reached the reviewer with an empty artifact list,
because the evidence compiler only collects script entrypoints and their Python
imports, never a data file consumed via input redirection. The reviewer, asked
whether the queried hosts were in scope, had no way to see them and fail-closed
on unresolved scope. Parse single `<` input redirections (not `<<` heredocs or
`<(` process substitution) and attach each workspace-resident file as an
artifact with role "input", bounded by max_artifact_bytes and flagged when
truncated. Files outside /workspace are not read.
Separately, the reviewer treated scope as the exact authorized host, so it
blocked resolving admin.fiuu.com under an authorized fiuu.com. State in the
prompt that an authorized domain covers its subdomains, and point the reviewer
at the new role "input" artifacts for scope checks.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two guarded-mode false-positives surfaced in real scan traces.
The reviewer blocked a boolean SQL injection probe
(`curl "…/login?username='+OR+'1'='1"`) for being an injection attempt at all,
though it is a read-only GET that changes nothing. The prompt said "allow only
non-destructive" but never established that in-scope offensive testing is the
tool's authorized purpose, so the model blocked on the technique. Rewrite the
guarded-mode guidance to judge by effect: in-scope injection probes, recon,
enumeration, and fuzzing pass, while destructive or persistent effects block —
with SQL spelled out (boolean/UNION/time-based read probes pass; DROP, DELETE,
INSERT, INTO OUTFILE, stacked statements, and command execution block).
Ambiguous evidence still fails closed, and every deterministic block, the
completeness gate, observe's passive-only rule, and scope enforcement are kept.
Separately the reviewer blocked a plain `curl` as "use of bash shell within a
curl command". The shell wrapper stamps `shell: bash` onto every exec_command
for execution, and the evidence packet passed that transport default straight
to the reviewer, which read it as the agent invoking a shell. Strip the
harness-injected transport keys (`shell`, `max_output_tokens`) from the packet's
original_arguments; the command itself is still parsed from `cmd`, so an
agent-authored `bash -c` payload is unaffected.
Note: the effect-based prompt also lets in-scope recon tools (nmap, subfinder,
ffuf, katana) through, which the old prompt blocked as "scanning" or "high
volume". That follows directly from judging by effect rather than technique.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Grouped browser verbs were classified by their verb alone. `tab` and `session`
sit in the passive set, so `tab new <url>` — documented as navigating — and
`session clear` earned a deterministic allow and executed unreviewed in guarded
mode and unblocked in observe, while `open <url>`, the same navigation, was
reviewed. Passivity is now decided from verb plus subcommand, and the packet
carries the result so observe mode stops maintaining a second copy of the rule
that could drift more permissive than guarded. The blocked-action list still
matches on the bare verb, so `auth login` keeps matching `auth`.
Interpreters were a seven-name allowlist, so `python3.12`, `/usr/bin/python3`,
`php`, and `deno` set no script path and produced a packet with no artifacts
that was still stamped complete — the exact shape the reviewer is told it may
allow. Recognize versioned and common interpreters so their sources are
actually collected, and fail closed when a command runs code that cannot be
resolved to an inspectable script.
`from pkg import payload` collected only the package initializer, because an
imported name was treated as an attribute and never as a submodule. Effectful
code in `pkg/payload.py` executed without appearing in the evidence.
Workspace staging runs twice per run and was not idempotent: the second pass
read the origin from `source_path`, which the first pass had already rewritten
to the copy. With the completion marker absent it cleared the destination and
then copied from that same emptied directory, silently handing the agent an
empty workspace. The origin is now read back from `original_source_path`.
Each fix is covered by a test that fails when the fix is reverted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The `agent_browser` skill is always loaded, so its safety paragraph shipped to
`off`-mode agents. Its prohibitions do not hold there — Strix only assigns a
browser session in a safety mode, while multi-session browsing is a normal
documented workflow — so the paragraph misdescribed the tools those agents
have. Move it into the already mode-gated block in the system prompt, and pin
the gating in both directions.
Test changes:
- `test_observe_mode_blocks_browser_click` asserted nothing about observe mode.
The same call blocks in guarded for a different reason (no prior snapshot),
so the observe rule was never reached. Give it a snapshot and assert the
block's source and category, plus the passive-read inverse.
- Neither workspace-epoch bump was pinned; removing either left the suite
green. Both are now covered, along with the read-only case that must not
bump, and an end-to-end pairing where a patch during review invalidates a
script decision.
- Cover `invoke_mutating_tool`'s observe-block and off-mode paths, the
reviewer's low-confidence, block, missing-model and failed-inspection rules,
the inline `bash -c` source path, and the two dependency-budget guards.
- Assert browser sessions are disjoint across agents rather than freezing one
agent's command string.
- Fold the compound-separator and safety-config tests into the parametrized
cases that already covered them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Introduce a pre-execution safety layer that reviews effectful agent actions
against compiled, frozen evidence before they run.
`--safety-mode guarded` allows non-destructive interaction after review;
`--safety-mode observe` permits passive target interaction only. `off` stays
the default, so existing runs are unchanged.
Deterministic rules decide what they can on their own: destructive commands,
code-loading environment overrides, blocked browser actions, and mutating
requests in observe mode are refused without a model call, and a small set of
read-only commands is allowed outright. Everything else compiles an evidence
packet — command, scope, script source and its local import closure, prior
tool-call evidence, and browser snapshot context — for a bounded reviewer that
may make one isolated inspection call. Incomplete evidence fails closed.
In safety modes, user-owned local directories are copied into the run
directory so the originals are never mounted writable, while `.git`,
`.agents`, and `.codex` inside the copy stay read-only.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>