Works with
Source: docs/guide.md · v0.1.2
yokoten guards nine AI coding agents with the same rules. This table is the one list of
them and how well each is covered. Each “yes” below is a real run: the agent tried
git push --tags in a throwaway repo, and the guard stopped it. “Not run end to end”
means the agent is listed, but its guard is not verified end to end yet. Docs are not
proof. The table is generated from the runtime contract in src/agents/runtimes/runtime.ts
and the dated runs. A test fails when the two differ.
| Agent | Hook file (init writes it) |
How it blocks | Capabilities (rewrite · stop · trust · fails · role) | Verified | Caveats |
|---|---|---|---|---|---|
| Claude Code | .claude/settings.json: PreToolUse, Stop and SubagentStop |
exit 2, the reason on stderr | yes · yes · yes · closed · yes | yes, 2026-10-19: headless, on DeepSeek deepseek-flash |
none |
| GitHub Copilot CLI | the same .claude/settings.json: Copilot reads it |
exit 2, and the reason as JSON on stdout | no · yes · yes · open · yes | yes, 2026-10-19: Copilot CLI 1.0.98, copilot -p, model gpt-6-luna (Auto) |
Runs repo hooks only after you trust the folder: run yokoten trust there. Skips them without a word otherwise. A hook that times out lets the call go on |
Antigravity CLI (agy) |
.agents/hooks.json: the yokoten entry, PreToolUse |
{"decision": "deny"} on stdout |
no · no · no · closed · yes | yes, 2026-10-19: agy 1.4.2, agy -p, model gemini-3.8-flash-high |
No stop check yet. Without .yokoten/, every call is refused: agy fails closed. The lead runs a worker’s done-when check |
| Codex CLI | .codex/hooks.json: PreToolUse and Stop |
exit 2, the reason on stderr | no · yes · yes · closed · yes | yes, 2026-10-19: Codex 0.163.0, codex exec, on DeepSeek deepseek-flash |
Runs the hooks only after you trust the folder, and then both hooks: run yokoten trust there. codex exec skips an untrusted hook without a word |
| Muse Code | .muse/hooks.json: PreToolUse and Stop |
exit 2, the reason on stderr | no · yes · yes · open · no | yes, 2026-10-19: Muse Code 1.4.10, muse exec --trust-workspace, model muse-spark-1.3-contributor |
Runs the hooks only after you trust the folder: run yokoten trust. Muse passes no YOKOTEN_ROLE to a hook, so mark a worker’s worktree with yokoten worker mark <dir>. It is not known to pass YOKOTEN_RELEASE_APPROVAL either, so release outside Muse. A hook that fails or times out lets the call go on |
DeepSeek Harness (dsh) |
.dsh/hooks.json: PreToolUse, and .dsh/yokoten.patch.yml to load it |
exit 2, the reason on stderr | no · no · no · open · yes | yes, 2026-10-20: dsh 0.2.1-rc.2, dsh headless --patch .dsh/yokoten.patch.yml, default DeepSeek model |
Runs hooks only through a bridge you add to each profile: see DeepSeek Harness. Without it, or without the patch, dsh runs no hooks and says nothing. Not guarded on Windows yet (#332). No stop check: the lead runs a worker’s done-when check |
Grok CLI (grok) |
.grok/hooks/yokoten.json: PreToolUse and Stop |
permissionDecision: "deny" on stdout, and exit 2 |
yes · yes · yes · closed · yes | yes, 2026-10-20: Grok CLI 1.0.52, grok -p --trust --always-approve, default model |
Runs project hooks only after you trust the folder: run yokoten trust there. Skips them without a word otherwise. Also runs the hooks in .claude/settings.json, so the guard may run twice for one call, on purpose |
| Gemini CLI | .gemini/settings.json: BeforeTool and AfterAgent |
exit 2, the reason on stderr | no · yes · yes · closed · no | not run end to end: personal sign-in retired 2026-06-18; needs a paid API key or an org license | Google’s agent for individuals is now Antigravity CLI |
| OpenCode | .opencode/plugins/yokoten.ts: OpenCode 2 permission evaluate, and OpenCode 1 tool.execute.before |
OpenCode 2 sets effect to deny; OpenCode 1 throws the reason |
no · no · no · closed · no | yes, 2026-10-23: OpenCode 2.1.4, opencode-go/deepseek-v4.1-flash, the plugin as init writes it; 2026-10-25: the guard stopped a command from opencode-go/glm-5.3-flash (GLM-5.3 Flash, by Z.ai) |
No stop check. A guard that fails or times out refuses the call. So does a call that is not read-only, when no command or path can be read. A subagent call was not part of that run |
| Any other agent | none | the rules in AGENTS.md, and the commit check, which stops a bad commit whoever made it |
— |
install writes the user hooks: the guide at the start of each session, and the command
guard. They go in ~/.claude/settings.json, and in ~/.codex/hooks.json and
~/.gemini/settings.json when those folders exist. Muse Code gets both, in
~/.config/muse/settings.json ($XDG_CONFIG_HOME/muse/settings.json when that is set),
when its config folder exists. Grok CLI gets only the command guard,
in ~/.grok/hooks/yokoten.json, when ~/.grok exists: it shows a session-start hook’s
output to you, not to the agent. Antigravity CLI gets only the command guard, in
~/.gemini/config/hooks.json, when ~/.gemini/config exists. Copilot CLI gets it in
~/.copilot/hooks/yokoten.json when ~/.copilot exists. Neither has a stop check yet:
agy’s Stop event has no re-entry signal, and Copilot lets a timed-out hook go on.
OpenCode gets the command guard in
~/.config/opencode/plugins/yokoten.ts when ~/.config/opencode exists. doctor counts every folder as trusted when
Grok’s trust gate is off: GROK_FOLDER_TRUST=0, or [folder_trust] enabled = false in
~/.grok/config.toml. In Codex, the guard checks the files
of an apply_patch, also one run through the shell.
When CLAUDE_CONFIG_DIR, CODEX_HOME or XDG_CONFIG_HOME is a full path, install writes in that folder instead, where the tool reads.
Each agent runs a project hook only after you trust it:
- Claude Code runs project hooks in a folder you trust.
- Copilot CLI runs them in a folder you trust. In
copilot -p, setCOPILOT_ALLOW_ALL=true: that exact value trusts the folder.--allow-all-toolsalone does not, and then the hooks do not run. - Antigravity CLI ran
.agents/hooks.jsoninagy -p, in a folder not on its trust list. It runs the hook with the.agentsfolder as its working folder. Guides say--dangerously-skip-permissionsskips the hooks. In agy 1.4.2 the hook still ran with it. Don’t count on either. - Codex runs project hooks only in a trusted project. Then approve each hook in
/hooks, or runyokoten trustin the folder. Codex remembers the command, so a changed command must be approved again;yokoten trustwrites its new hash. - Muse Code runs
.muse/hooks.jsononly in a trusted workspace, and skips it without a word otherwise. Inmuse exec,--trust-workspacetrusts the folder for that run. It also reads the project’s skills in.agents/skillsonly then. Outside a desk, Muse Code can read ~/.claude/CLAUDE.md and send that file to Muse’s model. Run Muse in a desk to keep your home folder hidden.doctorsays so when that file is there. When a desk can still see the file,doctorsays the home folder is visible and the desk needs attention. - Grok CLI runs
.grok/hooks/only in a trusted folder, and skips it without a word otherwise. Runyokoten trustin the folder, or type/hooks-trustin a session there.yokoten trustwrites Grok’s own trust file, for this folder only. Grok also runs the hooks in.claude/settings.json, so a call may be checked twice. That is on purpose. Grok can skip its own hook: in a folder it does not trust, or when a repo commits an empty hook file. The Claude Code hook then still guards the call. The second check costs about 0.1 s. - Gemini CLI runs project hooks only in a trusted folder. Start Gemini at the repo
root: it reads
.gemini/settings.jsonfrom the folder it starts in, and nowhere else. It warns once before it runs a new or changed hook. - OpenCode loads the plugin in
.opencode/plugins/when it starts. OpenCode 2 denies in the permission hook. OpenCode 1 still blocks on a throw. The lead ran this end to end on 2026-10-23, in a scratch repo with OpenCode 2.1.4 andopencode-go/deepseek-v4.1-flash; a subagent call was not part of that run.
init prints the one step that trusts the folder for each agent that has one. doctor then reads
each agent’s own trust store, read-only, and notes a folder it does not trust yet:
| Agent | Trust store | Counts as trusted |
|---|---|---|
| Copilot CLI | trustedFolders in ~/.copilot/config.json (or $COPILOT_HOME) |
the folder, or a folder above it |
| Codex | ~/.codex/config.toml (or $CODEX_HOME): [projects] and [hooks.state] |
the repo folder exactly, and each hook as it is now |
| Muse Code | ~/.config/muse/trust.json (or $XDG_CONFIG_HOME/muse/trust.json) |
the repo folder exactly |
| Grok CLI | ~/.grok/trusted_folders.toml (or $GROK_HOME): [folders."<folder>"] with trusted = true |
the repo folder exactly |
Doctor checks an agent only when its home folder is there (~/.copilot, ~/.codex, ~/.config/muse, ~/.grok).
When that folder has no store, or one doctor can’t read, it says “can’t check”, never ok.
A worker is a session with YOKOTEN_ROLE=worker. In Codex, a subagent whose
agent_type names a worker is a worker too. Copilot CLI and Antigravity CLI name no
subagent role in a hook call, nor does DeepSeek Harness: YOKOTEN_ROLE decides. Gemini CLI’s strict environment
sanitization removes YOKOTEN_ROLE from the hooks. Then yokoten can’t tell a worker in
Gemini CLI: every session counts as the lead, and the lead-only guard does not stop a
worker’s edit. Muse Code passes a hook only a short list of variables, and YOKOTEN_ROLE is not
one of them. Grok CLI passes YOKOTEN_ROLE to its hooks, and a subagent whose
subagentType names a worker is a worker too.
So the lead also marks each worker’s worktree: yokoten worker mark <dir> (#555). The guard
then treats every agent in that worktree as a worker, in Gemini CLI and Muse Code too. The
mark is a file in the worktree’s own git folder (.git/worktrees/<name>/yokoten-worker).
The guard finds it with no git process. The mark only adds the worker role: with the mark,
YOKOTEN_ROLE=lead is still a worker. The tool guard, the stop check, the commit check and
the session start all read it. It fails closed: a .git file the guard can’t read (a mode it
can’t open, a FIFO, no gitdir: line), or a git folder it names that is gone or can’t be
read, makes the session a worker. A refusal that comes from the mark says so, and names the
main checkout, where the lead works.
The guard refuses a worker’s plain ways to change the mark: a command that writes, moves,
removes, links or changes the mode of a file in a git folder, or redirects into one. A glob
counts when it can match .git or the mark (chmod 000 .gi?). So do git with GIT_DIR or
--git-dir, git worktree other than list, and an edit inside a git folder, by real path.
A read that names .git passes (grep --exclude-dir=.git, find . -path ./.git -prune).
This reads text, like the rest of the guard, so a worker that builds a command in pieces
gets past it. The real lock is the server and the credentials: branch protection and code
owners, and a worker that holds no push or bot token.
A worker commits and reports; the lead pushes and posts after review. For a worker the
guard refuses git push, git send-pack, git subtree push, a git alias (git config alias.*, git -c alias.*), yokoten bot in any form, gh pr
create|merge|edit|close|comment|review|ready|reopen, and gh api with a write method or
fields (a GraphQL query that reads passes). The main checkout is never marked, so the lead
there is not affected. In a marked worktree the lead counts as a worker too. git worktree remove <dir> removes the mark with the worktree. Without a mark, an agent that drops
YOKOTEN_ROLE counts as the lead.
Cancelling or deleting an Actions run is refused for the lead and for a worker.
The guard stops gh run cancel, gh run delete, and the same call through gh api
or yokoten bot. gh run rerun, view, list and watch still pass. Ask the
owner, who can cancel or delete the run on the Actions page.
A repositories/<id> path is the same call. A path with . or .. is read after those are resolved.
The route words match in any case, so Actions and Cancel are the same call.
A cluster of short options in gh api is refused, unless it is one option with its value joined.
A method or a path taken from the shell is refused when it can name a run.
A gh run subcommand taken from the shell is refused.
gh must name one of its own commands. gh extension install, upgrade, exec and browse are refused.
exec runs an installed extension. browse can install one.
A non-GET call is refused when the path after repos/ or repositories/ is only a variable.
gh config set aliases is refused. gh alias list and gh extension list still pass.
gh alias set and gh alias import are refused for both roles. Ask the owner to add an alias by hand.
No agent, lead or worker, gets gh’s token (#659, #668). With it, curl or a script could
call GitHub directly, around every rule above. gh and yokoten bot use the token without
showing it. So for every agent the guard refuses:
- gh’s commands that print it:
gh auth token,gh auth status --show-token(-t),gh auth git-credentialandgh config get oauth_token. - The places a login is kept:
git credential fill, a credential helper’sget(git credential-manager get),git -c credential.…,~/.git-credentials, gh’shosts.yml,security find-…-password -wandsecret-tool lookup. - The token’s variables:
$GH_TOKEN,$GITHUB_TOKEN,$GH_ENTERPRISE_TOKENand$GITHUB_ENTERPRISE_TOKEN, also as$env:GH_TOKEN(PowerShell) and%GH_TOKEN%(cmd). So arenode,python,jqand other programs that run code when the command names one. To write the name in a post, use a body file; to search for it, leave out the$. GIT_TRACE_REDACT=0, which makes a git trace print the token.- For the bot’s git,
-cand git settings in the environment (GIT_CONFIG_*). The bot also drops the pager, editor, browser, ssh and askpass programs and the trace and debug settings from its environment, and pages throughcat.
gh auth status, gh pr view and curl https://example.com still pass.
gh workflow disable is refused for every agent too: a workflow that is off runs no
checks on any pull request. Only the owner turns one off.
The guard stops a worker’s shell writes to lead-only files as well as its edits (#402). It
reads redirects (>, >>, >|, &>), sed -i, perl -i, tee, cp, mv,
install, ln, dd of=, truncate, rm, and git’s checkout, restore, rm, mv
and apply. A redirect counts with its file in quotes or glued to the word before it
(echo x>"AGENTS.md"). For git apply, the guard reads the patch to see its files, only
from a plain file under 1 MB; any other patch file stops a worker. A git commit that
would commit a lead-only file is stopped too: the commit check knows the role only from
YOKOTEN_ROLE, and a Claude Code subagent such as yokoten:worker has none. Reads such
as cat and grep pass. As for edits, this holds only in a repo that uses yokoten (one
with a yokoten.json). A path under .worktrees/<issue>/ reads from that worktree, as
if the session ran there: .worktrees/571-x/AGENTS.md is still lead-only (#571).
When the brief asks for a change to a lead-only file, the lead
starts the worker with YOKOTEN_LEAD_ONLY_OK=AGENTS.md,.github/workflows/ci.yml: paths
from the repo root, comma-separated, an entry with * read as a glob. * alone grants
every lead-only file. The guard and the commit check let those paths through. A subagent
shares the lead’s environment, so give such a task to a separate session. At session
start, doctor tells a session with YOKOTEN_ROLE=worker “this session is a worker:
lead-only files are guarded”. A separate worker session that does not hear it has no role
set. A subagent starts no session, so it hears nothing.
Sandboxes
init writes each agent’s sandbox for the project. doctor checks it in the
sandbox step. Start the worker in its worktree, so the sandbox covers it.
| Agent | What init writes |
Why |
|---|---|---|
| Claude Code | .claude/settings.json: sandbox.enabled is true |
Shell commands stay in the worktree. New network hosts ask first |
| Codex | .codex/config.toml: sandbox_mode is workspace-write, approval_policy is on-request, network_access is false |
Writes stay in the worktree. Network stays off. Codex asks before it goes beyond |
| Gemini | .gemini/settings.json: security.disableYoloMode is true |
YOLO mode, which approves everything, stays off |
| Grok | .grok/sandbox.toml: [profiles.yokoten] builds on workspace |
The yokoten profile keeps writes in the worktree. Workers run with grok --sandbox yokoten |
A value you set stays. doctor names what differs, with one fix.
Muse Code, DeepSeek Harness, Antigravity and Copilot have no project-writable sandbox, so init writes none for them. Muse has no project settings file for one. DeepSeek Harness and Antigravity keep only hooks in the project, and Copilot’s own sandbox is user-level. OpenCode gets a plugin, not a sandbox file.
DeepSeek Harness
DeepSeek Harness (dsh) runs Claude Code command hooks through a bridge plugin. The
bridge is not in the dsh package. init writes the hook and a launch patch, and prints
the rest. It installs nothing, and it leaves ~/.dsh alone.
-
Add the bridge and its peer to each profile you use (
tui,headless,web).dsh plugin addneedspnpmon your PATH. Without the peer, the bridge loads no hooks.dsh plugin --profile tui add @deepseek-ai/dsh-hooks-claude-code@0.2.1-rc.2 dsh plugin --profile tui add @deepseek-ai/dsh-hook-protocol@0.2.1-rc.2 -
Start dsh at the repo root, with the patch right after the profile name:
dsh tui --patch .dsh/yokoten.patch.yml dsh headless --patch .dsh/yokoten.patch.yml --json "<task>"The patch names
.dsh/hooks.jsonby a relative path, which dsh reads from the folder it starts in. Started anywhere else, or without the patch, dsh runs no hooks. yokoten owns.dsh/yokoten.patch.yml, andinitrewrites it. Put overlays of your own in another file.
dsh matches tool names exactly, in lowercase: the hook’s matcher is bash|pwsh|edit|write.
pwsh is dsh’s shell on Windows. It is in the matcher for later, but Windows is not
guarded yet. There dsh runs the hook itself in PowerShell, which can’t run yokoten’s
POSIX shell command. The bridge takes that failure as “go ahead”, so every call goes
through. On Windows doctor says so, and init does too (#332).
There is no stop check: the bridge’s Stop event has no loop guard. A worker on DeepSeek
Harness can report a task as done with failing checks. The lead runs the done-when
command (or the project’s checks when the brief has none). Antigravity CLI has no stop
check either; the lead runs the check there too. doctor checks both files, and looks in
${DSH_HOME:-~/.dsh}/profiles for the two packages. Without a dsh home it says it can’t
check. The bridge fails open without a word, so files in place prove nothing. Only a real
deny proves the guard runs: ask dsh to run git push --tags in a throwaway repo, and see
it refused.
Claude Code or Codex on DeepSeek
The guard sits in the agent, not in the model. So Claude Code and Codex keep the same
rules on a DeepSeek model. Both runs above used deepseek-flash.
Claude Code, through DeepSeek’s Anthropic-compatible API:
export CLAUDE_CONFIG_DIR=~/.config/claude-deepseek # a config folder with no claude.ai login
export ANTHROPIC_BASE_URL=https://api.deepseek.com/anthropic
export ANTHROPIC_API_KEY=<your DeepSeek key>
export ANTHROPIC_MODEL=deepseek-flash
claude
Keep the separate CLAUDE_CONFIG_DIR. With a claude.ai login in the config folder, Claude
Code sends that login’s token, not the key. DeepSeek answers 401 each time, and Claude
Code retries without a word until it times out. It looks like a hang. Do not use
--bare either: it skips the hooks, so the guard does not run.
Codex, through DeepSeek’s OpenAI-compatible API. Put this in $CODEX_HOME/config.toml
(default ~/.codex/config.toml), and set DEEPSEEK_API_KEY:
model = "deepseek-flash"
model_provider = "deepseek"
[model_providers.deepseek]
name = "DeepSeek"
base_url = "https://api.deepseek.com"
env_key = "DEEPSEEK_API_KEY"
wire_api = "responses"
Codex 0.163.0 refuses wire_api = "chat". It warns that it has no metadata for
deepseek-flash, and runs anyway.