Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 actionmanualplanauto
read or searchrunsrunsruns
edit a project fileasks youplan file onlyruns
edit a protected fileasks youplan file onlyasks you
run a commandasks you · runs in the sandboxnot availablethe judge decides (asks you if risky) · runs in the sandbox
use the internetasks younot availablethe judge decides (asks you if risky)
run a command outside the sandboxasks younot availableasks you
load a skillasks youasks youruns
start a sub-agentasks youasks youruns
the hard-deny listrefusedrefusedrefused
  • manual asks before anything but a read.
  • plan writes only its plan file and runs no commands.
  • auto runs 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-tools grants nothing. To make auto ask 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 read not allowed.
  • enabled = false: the command row ends · runs unconfined (sandbox off), and the two rows read no extra question: commands have it and no extra question: commands run outside.
  • Sandbox unavailable: a judging mode is not available in 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, and auto, --approve-all, run --away and harness eval work. --approve-all refuses 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:

reasoncoversthe flag reads
credentialsreading, printing, copying or sending a credential or a file holding onetouches credentials or secrets
irreversibledestroying work git can’t restore, or deleting a target the command doesn’t showmay destroy work git cannot restore
internet_codeinstalling or running internet codeinstalls or runs code from the internet
outside_coderunning a script or binary from outside the project, /tmp includedruns code from outside the project
outside_writewriting, moving or deleting outside the project and /tmptouches files outside the project
sends_data_outuploading or posting data to another hostmay send data to another machine
shared_statepushing, publishing, deploying, changing a remote resourcechanges something others rely on
safety_overrideusing the git stash or turning off a safety settinguses 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: the bash string or the joined exec argv, up to 8,000 characters.
  • facts: project_root and cwd (both $PROJECT, never a real path), plus current_branch and default_branch when they can be read from .git on 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*
  • dd writing 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 judge must also be in tools;
  • no tool may be in both ask and judge;
  • bash and exec must 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.