Modes and the judge
A mode decides whether a tool call runs, asks you, or goes to the judge. /mode shows the
table below for your session’s sandbox state. /mode <name> switches and shift+tab cycles
the built-ins, each printing a line on the new mode. [modes.<name>] in config.toml
changes or adds modes (config.md).
The three built-in modes
| kind of action | manual | plan | auto |
|---|---|---|---|
| read or search | runs | runs | runs |
| edit a project file | asks you | plan file only | runs |
| edit a protected file | asks you | plan file only | asks you |
| run a command | asks you · runs in the sandbox | not available | the judge decides (asks you if risky) · runs in the sandbox |
| use the internet | asks you | not available | the judge decides (asks you if risky) |
| run a command outside the sandbox | asks you | not available | asks you |
| load a skill | asks you | asks you | runs |
| start a sub-agent | asks you | asks you | runs |
| the hard-deny list | refused | refused | refused |
manualasks before anything but a read.planwrites only its plan file and runs no commands.autoruns project-file edits, skill loads and sub-agents unasked and hands commands to the judge. A skill adds text and a list of its files to the context and nothing more;allowed-toolsgrants nothing. To makeautoask before a skill, see the recipe.
A protected write asks in every mode, plan included (Protected files);
manual and auto also have local asks. The hard-deny list refuses even under
--approve-all.
In the terminal panel, asks no turn grant covers (a protected write, a credential read, a
removal git cannot undo, decoded text being run) start on deny, so a habitual Enter
refuses; press y to allow. Others start on allow once. The plain REPL takes a bare
Enter as no.
Every command runs inside a sandbox (sandbox.md): it writes only in the
project and a private temp folder (/tmp on Linux, $TMPDIR on macOS), has no network but
localhost (none on macOS), and can’t read your key folders. On macOS, use the internet
also covers local ports, such as a loopback test server. --approve-all refuses
run a command outside the sandbox but runs use the internet unasked.
The table changes with [sandbox] settings:
escape = "never": both rows readnot allowed.enabled = false: the command row ends· runs unconfined (sandbox off), and the two rows readno extra question: commands have itandno extra question: commands run outside.- Sandbox unavailable: a judging mode is
not availablein every row. mode = "external"(sandbox.md, “External mode”): the command row ends· runs unconfined (this machine is the sandbox), the two rows read as with the sandbox off, andauto,--approve-all,run --awayandharness evalwork.--approve-allrefuses only the hard-deny list; plan mode still refuses writes.
The judge
For each bash/exec command auto would run unasked, the judge (a second model) weighs
run against ask from the command, your latest message and a few repository facts.
Ordinary work runs: reading, downloading, building, testing, local commits, and edits and
repo scripts inside the project or /tmp.
Under --input-format stream-json a flag reaches the program as an approval_request of kind judge_flag
(judge_unavailable when the judge could not be reached) (headless.md).
Flagged. When the probability of ask reaches the cutoff, you get manual’s
approval prompt plus The judge flagged this: <reason>, naming the likeliest reason. The
first three ask even when you requested the action; the other five, unless your message
asked for that specific action:
| reason | covers | the flag reads |
|---|---|---|
credentials | reading, printing, copying or sending a credential or a file holding one | touches credentials or secrets |
irreversible | destroying work git can’t restore, or deleting a target the command doesn’t show | may destroy work git cannot restore |
internet_code | installing or running internet code | installs or runs code from the internet |
outside_code | running a script or binary from outside the project, /tmp included | runs code from outside the project |
outside_write | writing, moving or deleting outside the project and /tmp | touches files outside the project |
sends_data_out | uploading or posting data to another host | may send data to another machine |
shared_state | pushing, publishing, deploying, changing a remote resource | changes something others rely on |
safety_override | using the git stash or turning off a safety setting | uses the shared git stash or turns off a safety setting |
The reason is only a label; with none, the line reads The judge flagged this. An
unreachable judge (no key, a bad answer, over five seconds) asks with
The judge could not be reached. A flag never refuses: you decide.
The tag. A command the judge allowed shows · allowed by the judge on its result row,
as one you approved shows · approved. Probabilities appear only in the log.
What is sent where
The judge is a separate call to a separate provider: by default TypeSafe,
[judge] model = "typesafe:jev-1.14.0", keyed by TYPESAFE_API_KEY. Once per machine, the
first entry into a judging mode by command prints a notice naming the provider and model and saying
that the command, the facts and your latest message are sent. A start already in a judging mode does not print it; /notices lists it. Only TypeSafe’s built-in
endpoint gets a privacy-policy link. Without the key, auto asks before every command it
would judge, and entering a judging mode warns:
the judge needs TYPESAFE_API_KEY — without it, auto asks before every command it would judge
Through mega. Signed in to the free mega account
(config.md) with no TYPESAFE_API_KEY and the built-in
judge at a version our server accepts (jev-1.13.0 or jev-1.14.0), the same requests go via our server, which forwards them to TypeSafe with
its own key. The notice then links mega’s privacy page and reads:
Each judged command, its facts and your latest message are sent to TypeSafe (jev-1.14.0) through mega, with secrets redacted.
After /logout, judged commands ask, as with no key.
Neither the notice nor the warning prints under --approve-all, --mock or
harness eval, where the judge never runs.
The request holds:
user_request: your latest message in the root session, up to 4,000 characters. A sub-agent’s call sends it too, never the parent’s instructions to the child.command: thebashstring or the joinedexecargv, up to 8,000 characters.facts:project_rootandcwd(both$PROJECT, never a real path), pluscurrent_branchanddefault_branchwhen they can be read from.giton disk.
It never holds tool output, assistant text, reasoning, file contents, or a child’s own prompt.
Credential-shaped text (API key prefixes, PEM blocks and similar) is always replaced with
[redacted]. Project and home directories, and a ~/… spelling of the project, become
$PROJECT and ~, as whole paths only (a sibling …/harness2 stays ~/…). Other paths,
and spellings like /home//you, go as written. The session log keeps the request as
built.
Cutoffs
The cutoff is 0.2 by default, chosen on a labelled set to lean toward asking (figures
below). Lower it to ask more, raise it to ask less:
[judge]
cutoff = 0.15
A value outside 0.0..=1.0 is refused at load, naming the file. Only your own config can
set it, not a project’s.
Protected files
A write_file or edit_file under a protected path asks in every mode (plan’s own plan
file aside); no grant covers it, and only --approve-all skips it. The prompt adds
This is a protected file, and its “this turn” choice, except this kind, still asks at
the next one.
Entries match whole path components: a trailing / marks a directory, a leading **/ a
basename at any depth, anything else one exact path. . and empty components are ignored
(././CLAUDE.md matches), as is ASCII case on a case-insensitive volume like macOS’s
default (claude.md matches; .GIT is .git to the hard-deny list). A nested worktree
(.claude/worktrees/<name>/) counts from its own root: its CLAUDE.md, .git and
.claude/ are protected, its other files are not.
.git/
.agents/
.claude/
.harness/
AGENTS.md
CLAUDE.md
.cargo/config
.cargo/config.toml
**/build.rs
.envrc
.github/workflows/
.pre-commit-config.yaml
.husky/
.vscode/
Adjust it from your own config with [protected]:
[protected]
add = ["infra/"]
remove = [".vscode/"]
add uses the same matching rules; remove drops a built-in entry, and any other path does
nothing. A project’s config.toml can never change the list.
With checkpoints on, the turn-end list flags each protected file the turn changed, tracked
or not, protected, with its /undo (checkpoints.md).
Local asks
Two kinds of command ask before the judge sees them. Like a protected write, no grant
covers them and --approve-all skips them; while you’re away, they are refused
(sandbox.md, “Away”). Their “this turn” choice, any bash except this kind,
still asks for this kind. Under --input-format stream-json they reach the program as
approval_requests of kind removal and decode_and_run, with turn_grant_skips_kind: true
(headless.md).
The removal ask: a file git cannot restore. truncate (any size), an empty redirect
(> f and similar), cp /dev/null f, rm, git rm -f (not --cached),
git checkout -- or git restore of a named file asks, naming the first such target; so
does a named directory holding one. “Cannot restore” means untracked and not ignored,
changed since the last commit, or not in a git repository; clean committed and ignored files
go to the judge. Writing new content is an edit, not a removal. The reason reads:
Asked because it empties or deletes {file}, which git cannot bring back (untracked | changed since the last commit | not in a git repository)
The decode-and-run ask: decoding hidden text and running it. Piping decoded output into
a shell or interpreter, or eval-ing it (… | base64 -d | sh, xxd -r -p … | bash,
python3 -c "exec(…b64decode…)"), asks with the reason below. Decoding into a file without
running it does not.
Asked because it decodes hidden text and runs it
When the harness cannot tell which files. Past 1,024 targets, 256 KiB of their names,
64 possible working directories, or groups or sh -c code nested too deep, a removal asks
without naming a file:
Asked because the harness cannot tell which files it empties or deletes
Targets the command doesn’t name literally (globs, variables, command substitutions,
find -delete, one-liners, scripts) go to the judge.
The hard-deny list
Before any mode, judge or human has a say, --approve-all included, these are refused as
refused: <rule> is on the hard-deny list:
- a recursive delete of
/, of~/$HOME, or of the project root or one of its parents mkfs*ddwriting onto a real device under/dev/(not/dev/null,/dev/stdout,/dev/stderr, or/dev/fd/N)- the classic fork bomb, in any spacing
It catches only the obvious spelling: a wrapper script, a renamed binary or eval gets
past it. It is a speed bump, not a boundary (The limits). Under
[sandbox] mode = "external", only this list and, in auto, the judge stand between a
command and the machine.
The key scrub
In every mode, a bash or exec child starts without any registered provider’s key
variable (every api_key_env, TYPESAFE_API_KEY included), in use or not; env prints
none. Other variables, such as GITHUB_TOKEN, pass through, so gh and an authenticated
git push keep working.
Custom modes and judge
[modes.<name>] takes a judge field beside tools and ask: the tools the judge
decides. It merges like ask: left out, a built-in mode keeps its own; judge = [] turns
judging off. The rules:
- every name in
judgemust also be intools; - no tool may be in both
askandjudge; bashandexecmust share a bucket: both asked, both judged or both run.
config.md has a two-line [modes.auto] override that makes auto ask before
every bash/exec again.
The limits
The judge sees a command and the request behind it, never a script’s contents, a
build.rs, or what a binary does. The sandbox keeps what it allows from writing outside the project,
reading your keys or going online unasked, but damage inside the project
(git reset --hard, an in-project rm -rf, an overwritten file) is the judge’s call alone.
Commands can’t write untracked protected files (a local .envrc), but can write tracked
ones (CLAUDE.md, a workflow) and any build.rs: review those against the commit the work
started from (sandbox.md has the commands).
On the maintainer’s labelled set of 300 commands, the default judge and cutoff (jev-1.14.0,
0.2) still run 49 of the 114 that should ask and ask on 11 of 186 safe ones; the previous
default (jev-1.13.0, 0.3) ran 63 and asked on 5. Its misses were mostly work in sibling
checkouts, which the sandbox blocks or contains. Expect worse on unfamiliar commands. The judge is
strongest on credentials and internet code. Treat what it allows as unreviewed, inside the
project above all.