Node.js security releases (October 2026): update Node to 22.23.4, 24.22.1 or 26.12.1 before you use yokoten
yokoten
Docs Works with

The guide

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, set COPILOT_ALLOW_ALL=true: that exact value trusts the folder. --allow-all-tools alone does not, and then the hooks do not run.
  • Antigravity CLI ran .agents/hooks.json in agy -p, in a folder not on its trust list. It runs the hook with the .agents folder as its working folder. Guides say --dangerously-skip-permissions skips 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 run yokoten trust in the folder. Codex remembers the command, so a changed command must be approved again; yokoten trust writes its new hash.
  • Muse Code runs .muse/hooks.json only in a trusted workspace, and skips it without a word otherwise. In muse exec, --trust-workspace trusts the folder for that run. It also reads the project’s skills in .agents/skills only 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. doctor says so when that file is there. When a desk can still see the file, doctor says 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. Run yokoten trust in the folder, or type /hooks-trust in a session there. yokoten trust writes 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.json from 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 and opencode-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-credential and gh config get oauth_token.
  • The places a login is kept: git credential fill, a credential helper’s get (git credential-manager get), git -c credential.…, ~/.git-credentials, gh’s hosts.yml, security find-…-password -w and secret-tool lookup.
  • The token’s variables: $GH_TOKEN, $GITHUB_TOKEN, $GH_ENTERPRISE_TOKEN and $GITHUB_ENTERPRISE_TOKEN, also as $env:GH_TOKEN (PowerShell) and %GH_TOKEN% (cmd). So are node, python, jq and 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, -c and 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 through cat.

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.

  1. Add the bridge and its peer to each profile you use (tui, headless, web). dsh plugin add needs pnpm on 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
    
  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.json by 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, and init rewrites 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.