Let an external agent write¶
claude-cli and codex-cli can change files for you, but not in your checkout.
The run happens in a throwaway git worktree, its diff is stored as a pending
review, and a change reaches your repository only as a new local branch that you
approved by reading the diff and typing back part of its hash.
These are the only write paths for an external agent that keryx applies itself.
antigravity-cli refuses --write (see below).
gemini-acp --write is a different mechanism whose patch keryx never applies; see
the ACP client guide.
Everything on this page needs the external agent capability to be on, as for any
external child:
externalAgents.enabled: true in your user config, and the agent's CLI (claude
or codex) installed and logged in.
Run it¶
keryx agents external run claude-cli --task "Add a --json flag to the status command" --write
keryx agents external run codex-cli --task "Add a --json flag to the status command" --write
keryx cuts a worktree from the current commit (it needs a git checkout with at least one commit; uncommitted changes are not in it) and starts the agent there. The capture, review, flagged-path gate and landing below are identical for both agents. What confines the agent while it runs is different in kind.
claude-cli: a tool allow-list¶
claude runs with only the tools Read Grep Glob Edit Write: no shell, no
network, no MCP server. The launch flags are --tools Read Grep Glob Edit Write
--permission-mode acceptEdits --permission-prompts none, so an edit inside the
worktree goes through, a write outside it is denied, and any other question is
refused instead of asked.
codex-cli: an operating-system sandbox¶
codex has no tool allow-list. keryx runs it with -s workspace-write plus
-c sandbox_workspace_write.exclude_slash_tmp=true -c
sandbox_workspace_write.exclude_tmpdir_env_var=true -c
sandbox_workspace_write.network_access=false and --ignore-rules. Measured live on
2026-09-30 with codex 0.159.2, in a scratch git repository (not in a worktree cut
by keryx under the system temp directory):
- Writes outside the worktree (
/tmp,/var/tmp,$HOME, a sibling directory) fail with "read-only file system". - The network looked closed: a DNS lookup failed. Other routes were not tried.
.gitis read-only: no commit, no hook write.
Without the two exclude_* flags the default workspace-write sandbox lets
codex write to /tmp, which is why keryx always passes them. --ignore-rules is
just as mandatory: without it, the exec-policy rules in your own codex
configuration can let shell commands run outside the sandbox (writes to /tmp and
$HOME succeeded in the test); with it they were refused.
codex validates its final answer against a schema through OpenAI structured
output, which accepts a narrower JSON Schema than the subagent-result contract. keryx
therefore hands codex a strict copy of the contract (every property required,
optional ones nullable, closed objects, unsupported keywords removed), then drops the
nulls from the answer and validates it against the full contract. Only the final
message of the run is taken as the result; earlier narration is ignored.
codex keeps a sandboxed shell, so it can read what you can read
Unlike claude, codex still has a shell. Inside the sandbox it can read
any file your user account can read, for example keys under $HOME. The
network is closed as far as was measured, but their content can appear in the
run's output or in the diff. Review the diff and the run output before you
apply, and do not run a codex write task in a checkout or environment where
that matters.
keryx refuses a codex write run before it starts unless the installed codex is
0.159.2 or newer but older than 0.160.0. Older, newer, pre-release and unreadable
versions are all refused, and the message names the version found and the range
required. The ceiling is there because codex ignores a -c key it does not know
without saying so: a release that renamed one of the confinement keys above would
otherwise run unconfined and look fine. A newer codex becomes usable when a later
keryx release has measured it. A follow-up turn of a write run re-asserts the same
flags (the resume command line is unit-tested; write runs are started from the CLI
only, so keryx does not resume one itself).
Capture¶
When the run ends, however it ends (finished, timed out, crashed or interrupted with Ctrl+C), keryx captures the worktree's changes before removing it. The diff covers edited, new (including gitignored), deleted and mode-changed files. It is secret-redacted, hashed (sha256 of the redacted patch) and stored as a pending review under the session directory. Nothing reaches your checkout. If the agent changed nothing, nothing is stored and the report says so.
The report names the run id, the patch hash, the file list, any flagged paths, and the next command.
Review it¶
The review shows the run, the agent, the base commit, the patch hash, then the flagged paths first, the file list, and the redacted patch. It changes nothing. The run id is the session id of the run; a unique prefix works.
Flagged paths are changes under .git, .github, .claude, .metaproject,
hook directories (.husky, .githooks), CI configuration (for example
.circleci, .gitlab-ci.yml, Jenkinsfile, lefthook.yml), or files that change
what tools run or how they behave (.mcp.json, .envrc, .gitattributes,
.gitmodules, and .vscode/tasks.json, settings.json and launch.json), or
instructions and configuration for other agents (CLAUDE.md, AGENTS.md, .cursor,
.codex, .gemini). A bare entry named like one of these directories, such as a
symlink called .claude, is flagged too. They
are matched without regard to case, so .Claude/settings.json is flagged too. They
are the files that can run code later, so read them first.
The review prints the patch and every path with control characters (escape
sequences, a bare carriage return, and so on) shown as visible \xNN escapes, and
says so when it did that. The stored patch and its hash are not changed.
Apply it¶
keryx agents external apply <run-id>
keryx agents external apply <run-id> --allow-flagged # only when you mean it
apply needs a real terminal on both stdin and stdout. It shows the same review,
then asks you to type the first 12 hex digits of the patch hash (the digits after
sha256:, without that prefix). Anything else
cancels and nothing is applied. On a match keryx:
- creates a second throwaway worktree cut from the run's recorded base commit,
- applies the stored patch there (checked first),
- commits it as one commit with plumbing (no hooks, no signing), and
- publishes it as a NEW local branch
external/<run-id>.
Your current branch, HEAD, index and working tree are only read, never changed. A run lands at most once. Nothing is pushed and no pull request is opened; from there it is an ordinary local branch you can diff, test, merge or delete.
There is no flag or environment variable that answers the prompt for you, and a
caller without a terminal (a script, CI, a pipe) lands nothing: apply needs a
terminal: nobody can answer for you. --allow-flagged only lifts the flagged-path
refusal; the hash prompt still applies.
When apply refuses¶
| Refusal | Why |
|---|---|
redacted |
Redaction changed the patch, so landing it would land content the agent did not write. |
binary |
A binary file's content is not carried by the text patch. |
flagged |
The run changes flagged paths and --allow-flagged was not given. |
branch-exists |
external/<run-id> already exists. |
hash-mismatch |
The patch on disk no longer matches the hash recorded for it. (A wrong typed prefix is not a refusal: it cancels and nothing is applied.) |
not-pending |
The run was already landed or discarded, or was refused at capture. |
apply-failed |
git apply rejected the patch against the base commit. No branch is left behind. |
A run whose changed symlink points outside the worktree is refused at capture: no patch is kept, and it can only be discarded.
Discard it¶
Records the decision and deletes the stored patch. A landed or already discarded run cannot be discarded.
In the TUI¶
/external-diff opens the review modal over the pending runs. While at least one
diff awaits review, the sidebar shows one row, External diffs: N pending; clicking
it opens the same modal. The row is absent when nothing is pending.
| Key | Action |
|---|---|
| Up / Down | Select a run |
j / k, PageUp / PageDown |
Scroll the diff |
f |
Allow (or block again) flagged paths for the selected run |
a |
Apply: type the first 12 hex digits of the patch hash, then Enter; anything else cancels |
d, then y |
Discard the selected run |
| Esc | Close |
A run with flagged paths needs f before a will start. In the readline shell
(--no-tui), /external-diff only lists the pending runs and points at the
commands above; it never lands anything.
What this does not do¶
- No write for
antigravity-cli(agy). A live test showed its file-edit tool writes outside the working directory (to/tmpand into.git/hooks), and its headless shell is auto-denied only for commands, not for file edits, so keryx keeps refusing--writefor it. Write for Gemini is not planned. - No model review of the diff. The mandatory review is you, on the diff that is shown.
- No auto-approve, no unattended landing, no push and no pull request from the landed branch.
- No network and no MCP for the writing agent.
claudealso gets no shell;codexkeeps a sandboxed shell that can read files your account can read (see above). - Little live history. The
claudepermission flags were probed againstclaude2.1.280; thecodexconfinement was measured live oncodex0.159.2 in a scratch git repository. Neither agent has a long record of write runs, so treat the first runs as unproven and read every diff.