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 Install and commands

The guide

Install and commands

Source: docs/guide.md · v0.1.2

Requires Node 22.23.4 or later (24.22.1 or later on Node 24, 26.12.1 or later on Node 26), which runs TypeScript as it is. Releases 22.18 to 22.23.3 run TypeScript too, but do not use them: they have security holes that Node fixed on 2026-10-21 (advisory). So npm warns about them, and full doctor notes them under machine:. No dependencies. There are two ways to install: the Claude Code plugin, or the install command, from npm or from a clone. Use one, not both. The hook commands that yokoten writes need a POSIX shell: on Windows, use Git Bash.

Some people arrive with skills alone: npx skills add yokoten-labs/yokoten copies the skills only. A skill is advice to the agent; the guard is what stops a command. So skills alone guard nothing: run npx yokoten install too, or use the plugin, to turn the guard on.

Most people need one command: npx yokoten. It does the work of install, init and doctor: it sets up what is missing, then shows where you are and the next step. trust and update stay their own steps. yokoten help shows four commands and plain yokoten (Start here), and yokoten help --all lists the rest in groups; the tables under Commands explain each.

Which version to install

There are three release channels. Stable is the safe default.

Channel Plugin npm What you get For
Stable yokoten latest The latest release tag, such as v0.1.0, with a changelog Everyone
Beta yokoten-beta next The latest pre-release tag, such as v0.2.0-beta.1 People who want new features early and can report problems
Dev yokoten-dev none The default branch, every merged pull request Contributors, and our own tests

/plugin install yokoten@yokoten gets Stable. Use yokoten-beta@yokoten for Beta, or yokoten-dev@yokoten for Dev. Install one channel, not several. To switch, run /plugin uninstall yokoten@yokoten, then install the other name. From a shell, claude plugin uninstall and claude plugin install do the same. doctor names the channel and version at the top of its output.

A pre-release can break before the stable release. Dev changes on every merge and has no changelog.

The Claude Code plugin

In a Claude Code session:

/plugin marketplace add yokoten-labs/yokoten
/plugin install yokoten@yokoten

The first command adds this repo as a plugin marketplace. The second opens the plugin. Choose “Install for you” (user scope). From your shell, claude plugin marketplace add yokoten-labs/yokoten and then claude plugin install yokoten@yokoten do the same.

The plugin is this repo. It uses the same files that install copies, so the two ways can’t drift apart:

The plugin adds From
The skills: /yokoten:dispatch, /yokoten:inspect, /yokoten:merge, /yokoten:rca, /yokoten:record, /yokoten:release, /yokoten:yokoten kit/user/skills/
The agents: yokoten:worker, yokoten:reviewer, yokoten:verifier kit/user/agents/
/yokoten:init, /yokoten:doctor and /yokoten:update, which run yokoten from the plugin kit/plugin/skills/
The SessionStart doctor hook and the command guard: the same two hooks that install adds .claude-plugin/plugin.json

The command guard runs in every folder, so it keeps to the rules about commands, with one exception: an edit to a file inside a yokoten repo (one with yokoten.json) gets that repo’s file rules, wherever the session started. A worker in a worktree outside the session’s folder can’t change a lead-only file either.

Claude Code runs the plugin’s .ts files with node. Node can do this because the plugin folder is not under node_modules. To get a newer yokoten, open /plugin, select yokoten on the Installed tab, and choose Update now. A third-party marketplace does not update on its own until you turn on auto-update for it. From your shell, claude plugin update yokoten@yokoten does the same; for Beta or Dev, use yokoten-beta@yokoten or yokoten-dev@yokoten. The next tool call runs the updated guard. A session which still has the old hook command needs one restart. If Claude Code removes the old plugin folder while a session still runs, the tool call is refused and asks for a restart. When the plugin is older than the yokoten that runs doctor, doctor says so in a note.

The plugin works in Claude Code only. It does not make the private-marks file: doctor names the file, and you make it. When the plugin is on in your user settings, doctor counts its hooks and skills as installed.

Run install, or turn on the plugin: with the plugin on, install keeps its own copies out of your global folders. On a machine that had both before, the guard and the session doctor run twice and every skill shows up twice: doctor says so in a note, and install with the plugin on takes back the global copies it wrote. To move from install to the plugin, run yokoten uninstall. It removes what install wrote. Your own files in ~/.config/yokoten, such as the private marks you added, stay.

With the plugin on, install writes no guard of its own, because the plugin’s guard is the one guard. Turn the plugin off or remove it, and the machine has no guard at all: doctor then fails with “no guard for Claude Code”, and yokoten install writes install’s own guard.

Install with npx

For Codex CLI and Gemini CLI, or without the plugin:

cd your-project
npx yokoten             # once: installs what is missing, then the road

Run it again in any project and it does only what is still missing. One step at a time:

npx yokoten install      # this machine: skills, agents, hooks
npx yokoten init         # this project: adds files, never overwrites
npx yokoten doctor       # the road: where you are, what's next

npx downloads the package and runs it. The package has no dependencies. install and init copy yokoten’s runtime out of the package: to ~/.local/share/yokoten and to .yokoten/. The hooks run these copies, so they need no network and no npx.

The package runs built JavaScript, because Node does not run TypeScript under node_modules. The repo has no build; npm pack makes the package’s dist/ (record 0008).

The owner publishes the package, as the owner creates a release tag. The tag starts the release workflow. It runs the checks, then the publish job waits for the owner’s approval in the release environment. Only then does it publish to npm, with no npm token stored anywhere (record 0009). The first release is the one exception: the owner publishes it by hand, because npm can’t trust a workflow for a package it does not have yet. Until the first release is on npm, install from a clone.

Install from a clone

The same commands, from a copy of this repo. Use this way until the first release is on npm:

git clone https://github.com/yokoten-labs/yokoten
node yokoten/bin/yokoten.ts install      # this machine: skills, agents, hooks
cd your-project
node ~/.local/share/yokoten/bin/yokoten.ts init     # this project: adds files, never overwrites
node ~/.local/share/yokoten/bin/yokoten.ts doctor   # the road: where you are, what's next

yokoten’s messages say yokoten init and yokoten doctor. So install puts a small yokoten launcher in ~/.local/bin (yokoten.cmd on Windows). That folder is on the PATH on most setups, so yokoten works in the next shell you open. The launcher runs bin/yokoten.mjs, the same fast entry the hooks run. The next install rewrites a launcher that an older install wrote.

When ~/.local/bin is not on your PATH, install asks once: Add ~/.local/bin to your PATH in ~/.bashrc? [Y/n] (~/.zshrc for zsh, ~/.bash_profile for bash on macOS). On yes, it adds this one line to that file. On no, it prints the line for you to add:

export PATH="$HOME/.local/bin:$PATH" # added by yokoten

yokoten edits your shell profile only after that yes. It never asks, and never edits, when no one is at the terminal: in a pipe, a hook or CI. A ~/.local/bin/yokoten that yokoten did not write stays as it is, and install names it. uninstall removes the launcher and the marked line. doctor notes when no yokoten on your PATH runs the installed copy.

These commands, and the npx ones, are also in docs/facts.json, which the website reads, so a change to them goes in both places.

Use it as a GitHub Action

Any repo on GitHub can run yokoten’s checks in CI with one workflow file. Nothing is installed in the repo, and nothing leaves the job: no token, no outside service.

# .github/workflows/yokoten.yml
name: yokoten
on:
  pull_request:
    # `edited`, `labeled` and `milestoned` run `check --pr` again after a fix.
    types: [opened, edited, synchronize, reopened, labeled, unlabeled, milestoned, demilestoned]
  push:
    branches: [main]
permissions:
  contents: read
jobs:
  yokoten:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
        with:
          fetch-depth: 0
          persist-credentials: false
      - uses: yokoten-labs/yokoten@<full commit SHA>

The action sets up Node, then runs check. On a pull request it also runs check --pr. The findings show on the pull request page, as in yokoten’s own CI.

It handles three events:

  • push: check.
  • pull_request: check, then check --pr.
  • issues: check, then check --issue on the issue’s title. To run it, add issues: with types: [opened, edited] under on:.

On pull_request_target the checkout holds the base branch, not the pull request, so the action skips check --pr and leaves a notice: the pull request check runs on pull_request, not pull_request_target.

  • Pin it. Use a full commit SHA. A branch name like @main changes under you. Once there is a release, a release tag works too.
  • The checkout needs fetch-depth: 0. check --pr lists the files between the base and the head commit, so it needs both.
  • Permissions: contents: read is enough. The action takes no token.
  • Inputs: pr-check (default true), issue-check (default true) and node-version (default ^22.23.4, the first Node 22 with the October 2026 security fixes). Set pr-check: false to skip the pull request standard, issue-check: false to skip the issue title standard.
  • Node: the action sets up Node for the rest of the job too.

check --pr holds a pull request to the standard of core rule 8. A repo that does not follow it yet can start with pr-check: false. A pull request Dependabot opens gets the title check, and the other rules show as notes, because Dependabot writes its own body. The kit’s check workflow runs the project’s copy in .yokoten/ first: the same yokoten, and so the same check, as the commit hook. It then runs the action, pinned to the first release’s commit, so a pull request that changes .yokoten/ still meets a check it cannot edit; Dependabot keeps that pin current. The kit’s pr and issue workflows run the project’s copy.

A private repo without CI

A private repo on a free plan pays for its GitHub Actions minutes. Such a project can keep CI off until the repo goes public. Then the commit hook and yokoten check are the gate.

init reads the repo’s visibility through gh. On a private repo it says that the workflows use Actions minutes. It still writes them: init never makes this choice for you. To keep CI off, run:

yokoten init --no-ci

--no-ci does three things:

  • It adds "ci" to "notNow" in yokoten.json, with the reason.
  • It leaves out the three workflows and .github/dependabot.yml. Dependabot in the kit updates only the actions that CI runs.
  • It removes each of those files that is still the kit’s copy. A file you changed, or one reached through a symlink, is kept, and init names it.

With "notNow": { "ci": "<reason>" }, the road counts the commit hook and yokoten check as the gate. The steps that need CI wait with it: tests, ci and titles. The security-files step does not ask for Dependabot, and full doctor does not ask for a required CI check. A later init, or init --github --yes, writes no workflow again.

Without CI, some things are not checked. Full doctor says so once, in a no-ci note:

  • nothing on the server checks pull requests, issue titles or the workflows;
  • no code scanning, such as CodeQL, runs;
  • a commit made with the hook off reaches main unchecked.

So check each pull request before you merge it:

yokoten check --pr 12

check --pr <number> reads that pull request through gh. It checks the same things as in CI, with the commits in your clone. When a commit is missing, it says what to fetch.

From stage 5, the note also reminds you to end the choice when the repo goes public. When full doctor reads that the repo is public, it says so. To take CI up again, remove "ci" from "notNow" and run yokoten init. It writes the workflows, and the steps count CI again.

Commands

init copies the runtime into .yokoten/ in your project. Commit it: the hooks and CI then run with no network and no global install.

init keeps a project’s own setup working. An AGENTS.md without ## This project gets the core rules on top. Its own text moves under ## This project, and its headings go one level down; nothing else in it changes. init prints each heading it moved. When git has the old file as it was, init prints the command that gives it back. Otherwise it keeps a copy in AGENTS.md.before-yokoten. YAML front matter stays at the very top. An AGENTS.md that is a link stays as it is. With a CLAUDE.md, it adds an @AGENTS.md line at the end, because Claude Code reads AGENTS.md only through that import. A .claude/CLAUDE.md counts too, and gets @../AGENTS.md when there is no root one. With husky or another core.hooksPath, it keeps that folder and adds the commit check to its pre-commit. The project’s own hook lines stay, and init says which file it changed. git runs pre-merge-commit, not pre-commit, for a merge it commits itself, so init adds the same check there too (.githooks/pre-merge-commit, or husky’s .husky/pre-merge-commit). A .githooks/pre-commit of the project’s own gets the check too. A file under .husky/ keeps its mode, because husky 9 runs it with sh.

A clone can also have its own hooks in .git/hooks: from lefthook, the pre-commit framework, or written by hand. Pointing git at .githooks would switch all of them off, so init writes .githooks/pre-commit and .githooks/pre-merge-commit to run the check first and then the old hook, and for each other executable hook, such as commit-msg or pre-push, a .githooks file that only runs the old one. Each gets the same arguments and stdin. The old hooks stay in .git/hooks, and init names each one it kept. A hook that is not executable is skipped by git today, so init goes ahead and says how to keep it. When it can’t hand off (a commit hook comes from a global core.hooksPath, or the project’s own .githooks file has its check inside an if, or reads stdin like the old hook), init leaves core.hooksPath unset and prints what to do. With a global core.hooksPath, the files in .git/hooks never ran, so init leaves them out. doctor does not count the commit check as done while such a hook would be dropped. For a workflow of the project’s own that init keeps, doctor gives the step to add to it.

init also writes four workflows into .github/workflows/: yokoten.yml (the commit check, a lint job with actionlint and zizmor, and one ci job to make the required check), pr.yml, issue.yml and actions-health.yml. Each action is pinned to a commit, each job has a time limit, checkout keeps no token, and in yokoten.yml a new push cancels the old run on the same pull request. A push to main cancels nothing: every commit is tested on its own. pr.yml cancels nothing. Its runs can start in any order, so each one reads the pull request’s title, body, labels and milestone as they are now, with the job’s token, and a rerun reads them again. Add your tests as a job in yokoten.yml and to the needs list of ci. A private repo can keep CI off: see A private repo without CI.

yokoten.yml and pr.yml also run on merge_group, so a merge queue runs the same required checks as a pull request. A merge group run has no pull request. yokoten.yml runs every job in full, and the pr job skips, which counts as green for the queue. Turn the queue on in the repo settings. issue.yml never runs there: an issue is not a check on a merge.

actions-health.yml runs every 30 minutes. It reads the last 200 Actions runs and the checks of the open pull requests, then updates one pinned issue, CI health. The issue lists failed or cancelled runs on the default branch, failed or cancelled checks on open pull requests, slow workflows, queue waits, and branches that both passed and failed. It only writes that one issue. Delete the file to stop it, or run yokoten init --no-ci. yokoten ci-health prints the same report and writes nothing; the workflow runs it with --write.

When a workflow is already there, init checks whether you changed it. The kit keeps a list of every version it wrote. A file that matches one of them, Windows line ends or not, is a copy you never changed. init then updates it to the newest version and says so. init never commits, so git restore <file> brings the old one back. A file with your changes is kept as it is. init and full doctor show the command that compares it with the kit’s newest copy; doctor says it once for each kit version. A symlink is kept too: init never writes through one.

In yokoten.yml, a changes job reads which files a pull request changes. The pull request is docs-only when each file is under docs/ or changes/, or is a README.md or CHANGELOG.md. A job of yours can then skip a docs-only pull request; the comment above ci shows the two lines to add. ci counts that skip as green. The commit check and the lint job always run. A push to main runs every job. So does a pull request whose file list can’t be read: a warning says so. In yokoten’s own CI, a docs-only pull request runs the tests once, on Ubuntu with Node 24, and skips typecheck and the Action job.

It puts three safe defaults in .github/ too:

  • dependabot.yml updates the actions that CI runs, and the packages init finds (npm, cargo, pip, Go and more, at the root and one folder down). It opens one pull request a month for each, and takes a release only when it is a week old. A file you already have is kept; init names the packages it does not update.
  • SECURITY.md asks people to report a vulnerability privately, not in a public issue. For this, turn on private vulnerability reporting. doctor lists it with the other security switches.
  • CODEOWNERS names the owner for every lead-only path in the defaults: AGENTS.md, yokoten.json, .yokoten/, the hooks, the agent config and .github/. The guard stops a worker on the machine; this file stops the same change on the server. The owner is the account in a GitHub origin remote. Without one, the file has a placeholder: make the repo, add the remote and push, then doctor asks you to put your GitHub handle in it. GitHub takes no organization as an owner, so when gh says the account is one, the file has the placeholder too, and init asks for a person or @org/team. Full doctor flags a file whose only owner is the organization. It bites only when branch protection has “Require review from Code Owners” on. A CODEOWNERS file the project already has, in .github/, at the root or in docs/, is kept as it is.
Command Group What it does
install [--yes] [--list] Start One entry skill, yokoten, into ~/.claude/skills and ~/.agents/skills; agents into ~/.claude/agents; a SessionStart doctor hook; a user-level command guard; and an empty private-marks file. At a terminal it shows one screen of what changes, asks once, and then writes; --yes skips the question. It prints the files and settings keys it writes before it writes them. The two hooks go to Claude Code, and to Codex and Gemini CLI when ~/.codex or ~/.gemini already exists. With the Claude Code plugin on, it writes no global skill, agent or hook, and takes back what an older install wrote there. It adds the marks it finds on this machine, and writes a git mailmap. It asks before it sets git’s global mailmap.file. --list names every file it writes or keeps. See Privacy
init [dir] [--list] Start Puts the practices into a project. --list shows every file it wrote or kept; by default one screen does. Adds files; keeps any that are already there and says so. A kit workflow you never changed gets the newest version. It also reads GitHub through gh and says which standard labels and milestone it would add. It never changes GitHub. Without gh, a sign-in or a GitHub remote, it says what it skipped
init --github --yes [--remove-default-labels] [--board] Start Adds the missing standard labels, and a milestone “First release” when no milestone is open. When a closed milestone already has that title, the new one is “Next release”. A closed milestone is never reopened. A standard label that is there gets the standard colour and description. With 8 areas or fewer in yokoten.json, it adds one grey label per area. A second run changes nothing. GitHub’s default labels outside the set (question, wontfix and others) are deleted only with --remove-default-labels. --board also makes a board linked to the repo; it needs the gh token’s project scope
init --no-ci Start Keeps CI off: adds "ci" to "notNow" in yokoten.json, writes no workflow and no Dependabot config, and removes those files when they are still the kit’s copy. See A private repo without CI
trust [dir] [--undo] Start Turns on yokoten’s guard in the agents installed here, by writing their own trust for yokoten’s hooks: Codex, Grok CLI, Copilot CLI and Muse Code. A person runs it at a terminal; a pipe writes nothing. It trusts a folder only when yokoten’s own hook is there and the project’s Codex sandbox is safe, because trusting a folder is what lets its sandbox apply. init asks once on a terminal; with no terminal it writes nothing and names this command. --undo removes only the entries yokoten wrote, and leaves one you changed since. See Safe by default
doctor [--all] [--session] [--json] Start Where the project is on the road, in one screen: what is done, what is missing, and the next step. --all shows every stage and step, and what this machine is missing. --session prints only the next step, for the start of a session. Full doctor also runs the guard once on a trivial command and reports guard: 90 ms per command (60 ms CPU): the wall time and the CPU time (user plus system) of one call. A session skips it. With --agent gemini, it prints the step as the JSON that Gemini CLI reads
adopt [--all] [--json] Start One screen by default; --all shows the whole table, with a reason for every row. A draft of the adoption table for an old project: what it does today, and a proposed decision for each step. --json prints the rows. Writes nothing
explain [<id>] [--json] Start A rule or a step on the road: what it is, why, and how yokoten checks it
worker mark <dir> Work Marks the worktree at <dir> as a worker’s. The guard then treats every agent there as a worker, even one whose runtime drops YOKOTEN_ROLE (Muse Code). The guard refuses a worker’s plain ways to remove it, and a .git link it can’t read counts as marked; the server and credentials kept from the worker are the real lock. git worktree remove <dir> removes the mark. Given a subfolder, it marks the worktree’s top folder and says which. A plain clone, such as a desk, is marked too: the mark goes in its own .git (#672). The lead marks a clone from its own checkout. The main checkout (the clone the lead runs in, or one with worktrees) and a submodule are never marked. A worker can’t mark or unmark. See Works with
worker env [runtime] Work Lists the names that yokoten’s agent environment would keep from this shell, and counts the names it drops. With a runtime, its own login name counts too. It never shows a value. desk starts an agent with it. See What an agent’s environment holds
msg send <to> [<text>], msg inbox, msg who, msg log Work Agents of every vendor message each other through files on this machine. Only the lead runs log. See Messages between agents
desk [<issue> | "<task>"] [--agent <name>] [--pr] [--no-install] [--json] Work With no words, lists your desks and writes nothing (--json prints that list as data). With an issue or a task, makes the desk ../<repo>-desks/<n>/ (its own clone, branch desk/<n>), writes a brief, and starts the AI (Claude Code first) inside the fence. When the AI exits, the check and the tests run in the fence, and one screen shows the evidence. It never pushes; --pr pushes the branch and opens the pull request, as the bot when it is set up. A desk that stopped mid-task shows on the next start, and desk <n> goes on from its last commit. On native Windows, run it in WSL
desk <agent> <folder> [--name <name>] [--read <paths>] [--host <hosts>] [--marks] -- <command> Work Starts a worker’s agent in a fenced desk: its own clone, its own state folder, and no network but its model’s API. Only the lead runs it. See The desk
kanban [--text] [--json] [--no-open] Work Its old name board still runs it, after one line that names kanban. Serves a read-only page on this machine only: the sessions, the pull requests that wait on you, and Actions. Nothing on it starts, stops or changes work. It opens the page in your browser, signed in, and runs until you stop it (--no-open prints the address only). The sign-in key goes only to the browser. --text prints the same snapshot as plain text, and --json prints it as data. See The kanban
check [--staged] [--all] [--json] Guard One screen: the problems in full, the notes folded into a count; --all shows each note. Layout, privacy and lead-only files, for the tree or the next commit. A commit may not add build output or binaries. Each change file is read the way changelog reads it, so a wrong one fails at commit. With nothing staged, --staged notes it and is ok. For the tree, a translation behind its English source is a note. See Translations
check --pr Guard In CI: the pull request’s title, tier: line, sections, linked issue, change file, milestone and labels. See The changelog. The last line names each AI and model: 🤖 Made with <agent> (<model>), one or more comma-separated pairs. Reviewers may be added: 🤖 Made with worker <X> (<model>), reviewed by <Y> (<model>) and <Z> (<model>), lead Claude Code (<model>) (one or two reviewers, never more). A file that CODEOWNERS owns needs tier: A. The line is checked against the desk status files on the branch: a desk the line does not name, or reviewers who are all the writer’s own model, is a finding. One reviewer of another model is enough (record 0045): Claude Sonnet reviewing Claude Opus passes, and so does another vendor. A Made with model that disagrees with the worker’s status file is a note on the machine with its desk. CI cannot see that file, so CI never notes it. Long sentences on the lines a pull request adds to Markdown, hard-to-read names on the lines it adds to code, and a diff over 400 changed lines are notes
check --pr <number> Guard The same check for one pull request, read through gh, for a repo without CI. See A private repo without CI
check --issue Guard In CI: the issue’s title and labels. See Issue titles
check --anchor [--repo <owner/name>] Guard Each past UTC day of the event log against its hash in Sigstore Rekor, and names the day when the log no longer matches. Only the day’s 64 hex characters go to Rekor; no line, path or name leaves the machine. --repo names the repo whose anchor workflow signed the entries
provenance <ref> [--json] Guard For a commit, or each commit of a range: the writer, reviewers, approver, tier and issue, from the squashed message and the host’s latest Approve. --json prints the records. See Provenance
borrowed [--dry-run] [--json] Guard Compares each file in src/borrowed/ with upstream’s default branch, through gh, and opens one issue per source that changed. A security fix is called out in the title. --dry-run shows the report and opens nothing; --json prints the report as data. See Borrowed code
redact Guard Reads text on stdin and writes it with your details hidden: <owner>, <email>, <host>, <mark>. The tool-call guard sends risky commands through it. See Privacy
marks allow <mark> --here Guard Lets one of your private marks through in this repo only, with no yokoten.json. The allow is kept on this machine, never in the repo. --remove takes it back. See Privacy
anchor [--json] Guard Lists the anchors recorded in this checkout, newest first. A worker records one when its checks pass and its tree is clean. An anchor is a commit: go back with git switch -c try-2 <commit>, and nothing is reset or deleted. See The stop check
mcp [dir] [--command "<command>"] [--json] Guard Starts each stdio server in .mcp.json, .vscode/mcp.json or mcp.json and checks its tools for every client: schema, names, annotations, count, result shape and stray stdout. --command checks a server with no config entry; --json prints the findings. A Windows warning does not fail it. See Shipping an MCP server
check --history Guard The privacy check over every commit on every branch. Run it before a private repo goes public. A shallow clone can’t see old commits, so it fails without scanning; in CI, check out the whole history (fetch-depth: 0). A hit in a file names the commit that added it, and the path in that commit: file @ <commit>. It reads commits past git replace, and fails the same way when a grafts file can hide commits.
hook [--agent <name>] [--commands-only] [--denial <mode>] [--json] [--protocol] Guard The tool-call guard. Reads one tool call on stdin: a Claude Code PreToolUse call by default, a Codex PreToolUse call with --agent codex, a Gemini CLI BeforeTool call with --agent gemini, an Antigravity CLI PreToolUse call with --agent antigravity, a Muse Code PreToolUse call with --agent muse, a DeepSeek Harness PreToolUse call with --agent deepseek-harness, a Grok CLI PreToolUse call with --agent grok, an OpenCode tool call with --agent opencode. With --denial exit2, a refusal keeps exit 2 and the reason on stderr, for a host that reads the exit code alone. With --protocol, any tool sends the guard protocol and reads its answer: see the guard protocol
hook --stop [--agent <name>] Guard The stop check. Reads the end-of-turn event on stdin and checks the changed files: a Claude Code Stop or SubagentStop event by default, a Codex Stop event with --agent codex, a Muse Code Stop event with --agent muse, a Gemini CLI AfterAgent event with --agent gemini, a Grok CLI Stop event with --agent grok. A worker also runs the task’s done-when command. See The stop check
test [--heavy] [--print] [-- <runner words>] Guard Runs the tests light, for a machine other work shares (#461): node --test under nice -n 10 (not on Windows), with --test-concurrency at most half the cores and one per GB of free memory, and 1 at least. Words after -- go to the runner: test files, or its options. It is on by default. YOKOTEN_LIGHT_TESTS=0 turns it off for every call, --heavy for one. --print shows the command and why, and runs nothing
stops [off | on] Guard Shows the stop log at ~/.config/yokoten/stops.jsonl, and whether yokoten writes it. off stops the writing and deletes the log; on starts it again. The log stays on this computer. See The stop check
ci-health [--repo <owner/name>] [--write] [--dry-run] Merge and CI Reads the last 200 Actions runs and the checks of the open pull requests, and prints the report. It writes nothing. --write updates one pinned issue, CI health; --dry-run prints the report alone
pr sync Merge and CI Brings each open pull request up to date with its base branch after a merge. It merges the base in, resolves only two mechanical conflicts (both sides added a whole block, or both changed the same import list), runs typecheck and the tests of the touched files after every merge, clean or resolved, then pushes as the bot. Anything else stops with the files, the hunks and a dispatch brief. Each report line names the base it merged. The open list stops at 100 pull requests, and the report says when it did. It never force-pushes, rebases or resolves on GitHub. Run it after every merge. See Managing agents like a team
bot -- <command> Merge and CI Runs one command (gh, git push) as the bot, with a one-hour token it never prints. Before gh issue or gh pr create or edit with a title, it checks the title by the CI rules. A bad title is refused, and nothing is sent. gh api on an issue or pull request path is checked the same way. With --body or --body-file, gh pr create and gh pr edit also check the body by the body rules of check --pr, against the pull request’s branch and the base branch’s CODEOWNERS. A bad body is refused too, and nothing is sent. A body whose last line is not 🤖 Made with <agent> (<model>), one or more comma-separated pairs, is refused too: each pull request names each AI and model that made it. The line may add one reviewed by <agent> (<model>) part, or two reviewers joined by and; never more than two. A comment (gh pr comment, gh issue comment), a review with a body (gh pr review --body), and a new issue (gh issue create --body) carry the same line: a body whose last line is not the Made with line is refused too. A note with --attach is refused: the upload lands past the Made with line. An edit (gh pr edit --body, gh issue edit --body) stays as it is. gh pr create without a type or tier label gets them added: the type from the title (bug for fix), the tier from the body’s tier: line. gh pr edit does the same for a new title or body, and removes the old tier label when the body sets a new tier. A label you pass is kept. A tier label that does not match the body is refused, with the message of check --pr. A label the repo does not have is named in a note and left out; the bot never creates labels. gh pr create without --milestone gets the milestone of the first linked issue (Closes #N, Part of #N) that has one. When no linked issue has one, it is refused, and nothing is sent; add --milestone. When gh can’t read the issue, the pull request is sent without a milestone, with a note. It runs gh and git only, so env gh … or sh -c … is refused. It does not read a gh alias, so a title set through one is not checked. Setup: server.md
train [<pr>...] [--wait <seconds>] [--next] Merge and CI Merges an approved, green queue in the order given, as the bot, and stops at the first pull request that needs a person. With no numbers it only prints the plan from the conflict map, in that map’s order, and merges nothing; name the numbers to merge them. The order is: a change that moves a path first and alone, then a change that collides with nothing, then the one more others collide with, oldest first. It records every base before the first merge, so a pull request stacked on a squash-merged branch merges from the base it truly shares with that branch. A conflict made only of import lines takes the union; a real conflict, a failing test, a failing check or a missing approval stops the train. Each merged tree is tested before the push, and CI must pass on the pushed merge; --wait sets how long to wait (default 1800 seconds). --next says which pull request is next and why, and merges nothing. Only the lead runs it
conflicts [--json] [--rebuild <id>] Merge and CI Reads every open pull request, or every local branch when there is no host. It says in plain words which changes collide with main and with each other. It names the pairs that touch one file, and which of the two to merge first. It checks nothing out and merges nothing. It keeps the answer in the git folder, so the guard reads it without running git. --json prints the whole map, and --rebuild prints a prompt that rebuilds one change on the current main
update [dir] [--write] [--all] Maintain One row per changed rule by default; --all shows every changed line. Shows what this yokoten changes in the core rules of the project’s AGENTS.md. --write replaces them, and keeps the “This project” part as it is. See Updating the rules
upgrade Maintain Brings yokoten itself to the newest version. With the Claude Code plugin on, it updates the marketplace, then the plugin, over HTTPS, so no SSH key is needed. It adds and removes nothing. Without the plugin, it names npx yokoten@latest
changelog [--write], changelog --notes <version> Maintain Shows the entries the files in changes/ add under [Unreleased]. --write puts them into CHANGELOG.md and deletes the files; run it before a release. --notes prints a version’s release notes from its CHANGELOG.md section, so they are never written twice. See The changelog
learn <issue> [--write] [--all] Maintain One screen by default; --all shows every field the issue left open and the files the draft writes. Drafts a check from an incident issue, for a person to review. --write adds the files in lessons/. See Learning from an incident
learn --ship <id> Maintain In yokoten: moves a reviewed lesson into a group in kit/lessons/, where doctor from that yokoten runs it in every project. Refused while a TODO(reviewer) is left or its test fails
learn --export <id> [--out <file>] Maintain Writes a reviewed lesson as one small JSON file, with its incident and its positive control, and no private detail. --out writes the file instead of printing it
learn --import <file> [--write] Maintain Shows a lesson file from another project. --write adds it to lessons/ for review. Nothing runs before learn --ship
learn --upstream "<what happened>" [--area <name>] [--fix <idea>] Maintain Keeps a note about a gap in yokoten itself, with the project and you hidden. --list shows the notes; --done <id> removes one. See Lessons for yokoten itself
uninstall [--list] Maintain Takes back what install wrote: the entry skill and agents marked as managed by yokoten, its hooks in the settings of Claude Code, Codex CLI, Gemini CLI, Antigravity CLI, Copilot CLI, Grok CLI and Muse Code, its OpenCode plugin, and ~/.local/share/yokoten. It removes the private-marks and mailmap files when they hold only install’s header, and the folders and git config file it made when they are empty. A folder that was there before install stays. Your own files, edits and settings stay; the stop log is removed. A second run changes nothing. --list names every file it removes or keeps
tidy [--local | --empty [--yes]] [--json] Maintain Alone, it lists what is old, grouped by what you can do: merged or gone branches, worktrees and desks with no work left, and the logs of desks that are gone. Each row says why, and its size. It changes nothing (--json prints the list as data). --local saves each one first (a bundle of its branches, a patch of its unsaved changes, and index.json), then moves it into a dated folder in ~/.local/state/yokoten/trash. If a save fails, nothing moves. A desk or worktree in use is never touched. Branches on GitHub are only listed. --empty deletes trash folders older than 7 days, after one question; with no terminal it needs --yes

Each command prints its own help with --help or -h: what it does, its options and one example. yokoten help <command> prints the same. yokoten help lists the four commands to start with (desk, kanban, check and tidy; plain yokoten is the fifth), and help --all lists every command in groups by job. yokoten board, the old name of kanban, still runs it after one line that says so. yokoten version (or --version, -v) prints the version. Help changes nothing and exits 0. For a typo, yokoten names the closest command or option: yokoten docter says unknown command docter, then Next: `yokoten doctor`, if you meant it; `yokoten help` lists every command. After bot --, every word belongs to the command it runs, so yokoten bot -- gh --help runs gh --help.

check, doctor, hook, explain and adopt take --json. Then stdout holds JSON and nothing else, for an agent or a script. An error is JSON too: a usage error or a crash prints { command, ok: false, error } with exit 1, and the message still goes to stderr. Each finding has the same fields: id, what, why, fix, docs (the link) when the guide explains the rule, and file and line when it is in a file. yokoten explain <id> takes the id. No field holds a private value.

  • check --json: { check, ok, findings, notes }. check is tree, staged, history, pr or issue. A note never fails the check. When the CI run has no pull request or issue, check --pr and check --issue are ok and add skipped, the reason.
  • doctor --json: { version, ok, selfTest, repo, stage, stages, next, findings, steps, notes, server, lessons }. selfTest is { ok, failed, ran, skipped, reason? }: ok means no check failed; ran is false, with skipped and reason, when a control could not make a temp folder and did not run. The findings of a lesson are in notes; a new lesson has isNew: true. With --session, only the next step, the private-marks finding, and core-rules when the rules are behind. Then notes holds the work only on this machine and the findings of a lesson the session just heard about, if there are any.
  • hook --json: { blocked: true, finding } when it blocks, { blocked: false, toolCall: true } when it allows, and { blocked: false, toolCall: false } when stdin is not a tool call. The runtimes do not use it: they read the exit code and the reason on stderr, as their hook protocols say.
  • hook --stop --json: { blocked, stop, findings }. For a lead, stop_hook_active adds skipped, the reason. A worker is still checked when that field is set. When the check itself fails, skipped names that too. When a worker’s done-when check fails, also doneWhen. After the try limit, skipped is stopped with failing checks. That stop writes checks as failing in .yokoten-agent.json in a linked worktree. A pass there sets checks to passing. When stdin is not a Stop event: { blocked: false, stop: false }.
  • explain --json: { found, id, kind, what, why, check, docs, stage, standard }. For an unknown id: { id, found: false, ids }, exit 1. With no id: { ids }, exit 0.

Output for people follows one rule. Results go to stdout. Refusals, errors and the problems check finds go to stderr; a row of the doctor road, such as MISSING, is part of its result and stays on stdout. An unknown option is refused, never ignored.

init, update, doctor, check, adopt and bot need a git repo. Outside one, they say so, write nothing, and exit 1. init <dir> also refuses a folder that does not exist. doctor --session stays quiet outside a repo, so a session there gets no noise.

In CI, you don’t need to open the log to see what failed. When check runs in GitHub Actions, each finding is an annotation on the changed line of the pull request. The job summary lists each finding with its place, rule, what, why and the fix. The rule links to its section of this guide. A note shows as a warning and never fails the check. Like the rest of the output, these name a place, never what matched.

Exit codes

Code Means
0 All is well. Steps left on the road are to do, not problems, so doctor gives 0 with them. doctor --session always gives 0, so a session always starts
1 Problems found: by check, or by doctor (a failed self-test, or a core-rules, tree or machine line marked MISSING), an unknown id (explain), a usage error, or an error in yokoten itself
2 The hook blocked the tool call, or the stop check sent the agent back. The reason is on stderr

These codes do not change. yokoten itself gives 1 when it breaks. But only exit 2 blocks a call in all three agents, so a guard that crashed would let the call through. The tool-call hook that init, install and the plugin write turns any other exit into 2: when the guard crashes, runs out of memory or is killed, the call is refused with one line, yokoten's guard failed (exit N), so the command is refused; run `yokoten doctor`. That line is all of stderr: node’s own error and the shell’s “Killed” line are dropped. Two cases stay at 1: when the runtime is gone, or when node is not on the agent’s PATH, the hook says so in one line and the call goes on. A deleted folder or a missing node does not block every tool call, so you can still run yokoten doctor. The stop check and the session doctor pass a failure on as it is.

The runtimes also let a call through when a hook runs past their timeout, so when reading a call takes the guard longer than 5 seconds (YOKOTEN_GUARD_DEADLINE_MS changes it, up to 30 seconds), or fails, the guard refuses the call with 2 (unreadable-command).

Machine notes

Some problems on the machine stop every agent session, and the agent can’t see them. Full doctor lists them under machine: as notes. A note never changes the exit code. Record 0012 says which problems doctor looks for, and why.

  • disk-space: the disk that holds the repo is low on space. A full disk stops every session. Doctor warns below 20 GB, when that is also under 10% of the disk. Set YOKOTEN_MIN_FREE_GB to use another limit, or 0 to turn the warning off. A session hears this note too.

  • host-disk, in WSL 2 only: the Windows drive that holds the Linux disk file. Inside WSL, df shows the size of the disk file, not the free space on that drive. Full doctor reads the drive through reg.exe, and waits at most 3 seconds. It keeps the reading in ~/.config/yokoten/machine.json. A session runs no program: it repeats the last reading when that was low and is at most 7 days old. Run yokoten doctor to read the drive again. Without WSL_DISTRO_NAME, as under sudo, doctor can’t find the drive and says nothing about it. The VM’s disk file only grows. Deleting files inside the VM frees nothing on the Windows drive. So the note says: back up your work to another drive first. Then free space on the Windows drive, or shrink or move the disk file with Microsoft’s guide. yokoten does not suggest WSL’s sparse mode: WSL marks it unsafe. When the kept reading is low, the tool-call guard asks before a build, a test suite, a package install or a container build starts. Claude Code shows the question; the other agents get the same words as a note. The guard never refuses this, and reads only the kept reading.

  • agent-temp-dir: another user owns Claude Code’s temp folder, claude-<uid>. The folder is in CLAUDE_CODE_TMPDIR, else in TMPDIR, by default /tmp. This happens when a root process makes the folder first, for example after a restart. Claude Code then refuses to start. The note prints sudo rm -rf '<folder>', quoted. Run it yourself: doctor never deletes anything.

  • leftover-temp-folders: yokoten-* folders older than a day in the temp folder. Test runs leave them, and thousands of them fill the disk. Full doctor counts them. It does not add up their size, so it stays fast. The note prints a find ... -exec rm -rf command that removes only the old ones. Run it yourself: doctor never deletes anything.

  • node-security: the Node that runs yokoten lacks the security fixes of 2026-10-21. A release older than the fix in its line (22.23.4, 24.22.1, 26.12.1) has known holes. A line that gets no more fixes, such as 23, 25 or anything below 22, is not supported. The note names the version to install. Full doctor only, and never a failure.

The stop check

The tool-call hook reads commands. It does not see a file that sed or a script changed. The commit check sees it, but only at the commit. The stop check runs earlier: when the agent tries to end its turn. That is the Stop event in Claude Code, Codex, Muse Code and Grok CLI, SubagentStop in Claude Code, and AfterAgent in Gemini CLI.

flowchart LR
  S["agent ends its turn"] --> U{"subagent, and its type is not a worker?"}
  U -- yes --> N["turn ends, no check"]
  U -- no --> W{"a worker?"}
  W -- no --> A{"already sent back once?"}
  A -- yes --> E["turn ends"]
  A -- no --> C{"findings in changed files?"}
  C -- no --> E
  C -- yes --> B["agent gets the findings and fixes them"]
  W -- yes --> D{"done-when check passes?"}
  D -- yes --> E
  D -- no --> K{"try count saved?"}
  K -- no --> H{"stop_hook_active, or refused in the last 5 minutes?"}
  H -- yes --> X["stop allowed; checks still failing"]
  H -- no --> F["one refusal; the next stop is let through"]
  K -- yes --> T{"this try is 1, 2 or 3?"}
  T -- yes --> F2["worker gets the output and fixes it"]
  T -- no --> X
  • It checks the changed files only: staged, not staged, and new files that git does not ignore. It uses the privacy, secret and file rules of check --staged.
  • A subagent is checked only when its own type is a worker. The parent session’s role does not count. A reviewer started by a worker is not checked. Its stop exits 0. A type is a worker only when it is worker, or it ends with :worker. coworker and code-worker are not workers. Mark such an agent as worker, or use a name that ends with :worker. A reviewer, Explore agent or verifier is not checked.
  • For a lead, it sends the agent back once. The agent then sets stop_hook_active, and the next stop always ends the turn. There is no loop.
  • For a worker, it also runs the task’s done-when command. The command comes from YOKOTEN_DONE_WHEN, or from yokoten-done-when in the worktree’s git folder. The environment variable wins when both are set. When the brief has none, the changed-file check is the project’s check.
  • The command uses the platform shell. That is sh on Linux and macOS, and cmd.exe on Windows. It may run for two minutes. When it ends, or the time runs out, yokoten stops the command and the programs it started.
  • On Windows, cmd.exe runs in a job when Windows PowerShell is there. Closing the job stops those programs after cmd.exe has exited. The command reaches the host with -EncodedCommand, so the check’s own text and quotes stay as they are. A security policy on the machine can still block Add-Type, which compiles the short job script. If PowerShell is missing, blocked, stops before it starts the check, or cannot make the job, the check runs cmd.exe alone and says why on stderr. A program left behind can then keep running. A host that stops after it started the check is not detected: its exit code is read as the check’s own. The two-minute budget includes starting that host.
  • A program that has already started its own session is not stopped. setsid starts one. A new process group in the same session is stopped on Linux. On macOS, ps gives no session id, so only the check’s own process group is stopped.
  • A failing check sends the worker back, with the output in short and what to fix. The worker gets three tries. On the third, the hook says the next stop is let through. The worker’s final message to the lead must start with NOT DONE: checks failing. It must quote the failing output. The task is not done.
  • The allowed stop writes .yokoten-agent.json at the root of a linked worktree. It does not write that file in the main checkout. It sets checks to failing and updated to the time in UTC. A pass sets checks to passing. Other fields stay. The lead reads that file. Its /api/state shows it as the commit check. The allowed stop’s stderr reaches nobody. A status file yokoten cannot read is left unchanged, and the stop check says why.
  • When the try count cannot be saved, one refusal says the next stop is let through. The same worker is let through when it was refused in the last 5 minutes. This holds even when the runtime never sets stop_hook_active. A worker is never trapped. The count is cleared when the check passes, and again when the worker is let through. A resumed worker is checked from try 1. Count files older than a day are removed when a new count is written.
  • A subagent worker shares the lead’s folder and environment. It does not get its own YOKOTEN_DONE_WHEN. The check runs in the event’s folder. It runs in the worker’s worktree only when the stop event names that worktree.
  • DeepSeek Harness, Antigravity CLI and OpenCode have no stop hook. The lead runs the check.
  • Claude Code, Codex, Muse Code and Grok CLI each wait 600 seconds for this hook. Gemini CLI reads timeout in milliseconds, so its hook waits 600000 milliseconds. Each of those is 10 minutes. That covers the two-minute command. Muse Code settings hooks, including .muse/hooks.json, also read timeout in seconds.
  • It reads no history and uses no network.
  • On an error in yokoten, the turn ends and stderr gets one line.
  • A new file you want to keep out of the check goes in .gitignore, or in privacy.skip in yokoten.json.

Each stop is also written to a log on this computer. yokoten writes one line to ~/.config/yokoten/stops.jsonl when the guard stops a tool call, or the stop check sends an agent back. The line holds the time in UTC, the agent’s runtime, its role, the rule, and the answer: refused, allowed once (the worker was let through after its tries), or no answer (the guard could not read the call). A command is written only after the privacy filter has masked it, and only its first line; an edit writes no path and no file text. The file stays on this computer, and yokoten sends it nowhere. Old lines go first when it passes 5 MB. yokoten stops off stops the writing and deletes the log; yokoten stops on starts it again. yokoten uninstall removes the log too.

init adds the hook to .claude/settings.json (Stop and SubagentStop), .codex/hooks.json (Stop), .muse/hooks.json (Stop), .gemini/settings.json (AfterAgent) and .grok/hooks/yokoten.json (Stop). Claude Code, Codex, Muse Code and Grok CLI wait 600 seconds. Gemini CLI waits 600000 milliseconds. install does not add it at user level: it reads files, so each project turns it on, as with the file rules of the tool-call hook.

The commit check refuses a file in a dist/, build/ or out/ folder, and a binary file. A build makes such files again, and a reviewer can’t read a binary. Images and fonts (png, jpg, gif, webp, svg, ico, woff, woff2) pass. To keep a file anyway, list it in yokoten.json as a glob: "allowBuild": ["docs/manual.pdf"].

Blocked commands

A project’s stack rule, such as “no Python” or “no yarn”, is easy for an agent to forget in the middle of a task. Written only in AGENTS.md, it is a wish. List the programs in yokoten.json, each with its reason, and the tool-call guard holds the rule:

"commands": {
  "block": {
    "python": "this project is TypeScript only",
    "python3": "this project is TypeScript only"
  }
}

The guard then stops a command that runs python3, for the lead and for every worker. The message names the program, gives your reason, and tells the agent to use the project’s own stack.

  • The guard looks at the program of each command, by its name: /usr/bin/python3 and python3.exe count as python3.
  • Every part of the command counts: a && b, a | b, a; b, $(...), and the string a shell runs: bash -c '...', ssh host '...', flock file -c '...'.
  • Assignments, redirects, launchers and shell keywords do not hide the program: X=1 python3; 2>/dev/null python3; launchers such as env, sudo, nice, timeout, xargs, setsid and stdbuf; a command after if, for, while or !; a program find -exec runs; and the string in env -S.
  • A word that only holds the name passes: echo python, cat python.md, pythonic, and a case pattern such as python3). command -v python3 passes too: it only says where the program is.
  • A name has no folder and no space. A wrong shape in "commands" is a config error, as for any key in yokoten.json. The guard then blocks no program and says so, and the core checks still run.

The guard reads command text. It does not see a program that a script, a make target, an npm script or a shell function runs. Name each version you want to stop: python3.12 is not python3.

The safeguard’s own refusals are fixed, not a setting: a wipe of home or the system, a disk command, and root. Two of them can surprise you.

  • A wipe of a target the guard cannot read is refused, such as $OUT_DIR or $(pwd). Name the folder with a plain path instead.
  • A disk tool is refused even when it only reads: fdisk -l, parted -l, diskutil list and gpt -r show disk0. Ask the owner to run it.

The folder the command runs in counts as the project, so rm -rf dist passes there.

yokoten.json

yokoten.json at the repo root holds the project’s settings. Every key is optional. A missing key, a key set to null, or a missing file gives the default.

Key Must be Default
layout { "maxEntries": 7, "exempt": ["docs/records"] }: a whole number above 0, and a list of folders or globs 7, no folder exempt
tests a list of globs *.test.*, *.spec.*, *_test.go, test/**, tests/**
leadOnly a list of globs the rules, agent config, hooks and CI
privacy { "allow": [], "skip": [], "utc": true, "output": "run", "skipOwnName": true }: two lists of text, true or false, "run" or "stop", and true or false lockfiles skipped, UTC on, output run, own name skipped
notNow reasons by step name: { "ci": "no remote yet" } none
allowBuild a list of globs none
areas a list of words any lowercase word
translations { "fail": true } false
commands { "block": { "<program>": "<reason>" } } no program blocked
worker { "timeLimitMinutes": 180, "settings": ["SCREENS_OUT"] }: a whole number above 0, and a list of setting names 180 minutes, no extra setting

yokoten checks each key against this table, in one place:

  • A key with the wrong shape, such as "leadOnly": "x", is a config error. check and doctor exit 1. The message names each wrong key and says what it must look like, so one run shows them all.
  • The tool-call hook then uses that key’s default. It says so in one line on stderr, and the core checks still run. A wrong field in an object, such as layout.maxEntries, gives only that field’s default. A wrong "commands" drops the whole block list.
  • When yokoten.json is not valid JSON, or is not one JSON object, check and doctor report a config error. The hook then uses all the defaults, says so in one line, and still runs the core checks.
  • A key that yokoten does not know is a note from check, never an error. A config written for a newer yokoten then still works with an older one. A field it does not know inside an object, such as "commands": { "blok": ... }, is a note too.

Issue titles

An issue title names the area, then the work. A list of issues then reads like a list of commits to come.

  • A task: area: verb phrase, for example doctor: add the review step.
  • An incident: area: symptom, for example check: misses a key in a JSON file. Name what you saw, not the fix.
  • At most 60 characters. No full stop at the end. No #N: link other issues in the body.
  • The area is one lowercase word, or words joined by hyphens.
  • One type label (feat, bug, docs and the like) and one tier label (tier A, tier B or tier C), when the repo has those labels. A repo without type or tier labels gets no line.

init adds .github/workflows/issue.yml. It runs check --issue when an issue is opened or edited. It checks the title and the labels: one type label and one tier label, when the repo has those labels. It only reads: it never comments on the issue or renames it. A red run tells the author to fix the title or the labels. To know which kinds the repo has, the check lists its labels through gh; when gh cannot list them, only the title is checked.

An agent’s title is checked before it is sent. In a repo with a yokoten.json, the tool-call guard runs the same check on gh issue create|edit and gh pr create|edit with a title, plain or through yokoten bot. A bad title stops the command, so CI never sees it. yokoten bot also refuses a gh issue create with a missing type or tier label the repo has, so a bad issue is not sent. The user-level hook does not check titles: another project has its own rules.

Any lowercase area passes. To limit the areas, list them in yokoten.json: "areas": ["doctor", "check", "docs"]. Pull request titles then use the same list in type(area): summary.

The changelog

A pull request that changes what users notice adds one small file, changes/<issue>-<slug>.md. It does not edit CHANGELOG.md. When every pull request adds a line at the top of the same file, each merge makes the others conflict. Separate files never conflict.

The file is plain Markdown. Line 1 is the section: Added, Changed, Deprecated, Removed, Fixed, Security or Internal. Line 2 is blank. From line 3, write the entry as it should read in the changelog. The - is added for you. Several bullets in one file are fine. The entry has no headings, and the file is a plain file right in changes/, not in a folder or behind a link.

Fixed

`doctor` no longer stops when the repo has no remote (#42).

yokoten changelog shows what the files would add, and writes nothing. Before a release, yokoten changelog --write puts each entry under its section in [Unreleased], and deletes the files it used. It never touches released versions. If a file is wrong, it says which file and why, and writes nothing.

check --pr takes a new file in changes/ as the pull request’s changelog entry, and checks its form. A line in CHANGELOG.md still passes, for a project that has not moved yet. check reads the files in changes/ the same way, and check --staged reads the staged ones. So the git hook stops a wrong file at commit, before CI does. It also notes long sentences in a staged change file. A change file not yet added to git counts too, as it does for changelog. init adds changes/README.md, which shows the form. changes/ may hold more than 7 files, since each release empties it.

Translations

English is the source; a translation follows it. When the English text changes, the translation falls behind, and nobody sees it. So each translated file names its source on line 1, in an HTML comment. GitHub does not show it.

<!-- source: docs/guide.md -->

The path is the English file, from the repo root. Only a relative path makes a marker: an attribution comment such as <!-- source: https://example.com/post -->, or an absolute path, is left alone. Write this line once; it never changes. When you change the English, update the translation in the same commit or pull request.

check reads line 1 of each Markdown and HTML file. Then it reads the commits on the current branch, first parent only, back to the commit that added the translation. A commit that changes the English but not the translation puts the translation one commit behind. Only a later commit that changes only the translation catches up. A commit that changes both files is fine, but it does not cover an English-only commit before it. So when one pull request changes only the English and another changes both, the first is noted, whichever merges first. Any line of the translation counts.

Why not a line that records the English version? Every pull request that changes the English would then edit that line. Two such pull requests would always conflict on it. With a line that never changes, parallel pull requests touch only the paragraphs they translate.

The costs. First, a commit that changes only the translation catches up with every English change before it. A typo fix in the translation can hide an English change that nobody translated. So run check before a typo fix. If it notes the translation as behind, update the translation first. Second, with squash merges, an English-only pull request stays noted until a commit changes only the translation. This is true even when a later pull request translated it along with its own change. To clear the note, update the translation in a pull request of its own.

In the work tree, an edit to the translation alone counts as a later change. An edit to the English alone makes the translation behind. An edit to both counts as a commit that changes both.

When a translation is behind, check gives a translation-behind note. The note says how many commits behind, and how to see the change: git diff <commit> HEAD -- docs/guide.md. A note never fails CI. To make it fail, put "translations": { "fail": true } in yokoten.json.

An older form of line 1 adds the English blob: <!-- source: docs/guide.md blob 3b18e512dba79e4c8300dd08aeb37f8e728b8dad -->. check still reads it. A blob equal to the English text is current; otherwise the commits decide. You can drop the blob at any time.

A shallow clone holds only the newest commits. When they are enough to tell, check answers as usual; an English-only commit it sees is “at least” that many commits behind. When they are not enough, check gives one note for the whole run. The note says how many translations weren’t checked, and there is no note for each file. That note never fails, even with "fail": true. check never fetches. In CI, use fetch-depth: 0; the kit’s yokoten.yml does.

Borrowed code

Record 0024 says when yokoten may copy code from another project. A copied piece is a wheel. A wheel lives in src/borrowed/, never as a dependency. It is small: only the part we need. The code under it is not the project’s own, so a reader must see where it came from.

Copy only under a permissive license: MIT, BSD-2-Clause, BSD-3-Clause, ISC or Apache-2.0. For any other license, use the clean-room road: one agent writes what the code does, and another agent, which has not seen the source, writes the code.

Every wheel starts with a comment header, before any code:

// source: https://github.com/example/upstream/blob/3f2a1c9d0e8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f/src/thing.ts
// commit: 3f2a1c9d0e8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f
// license: MIT
// copyright: Copyright (c) 2026 Example Authors
// changes: renamed readThing to parseThing
Field Needed What it holds
source always The upstream file URL, as a github.com/<owner>/<repo>/blob/... link
commit always The full 40-character commit sha the code was taken from
license always The SPDX name: MIT, BSD-2-Clause, BSD-3-Clause, ISC or Apache-2.0
copyright always The upstream copyright notice, word for word
notice Apache-2.0 The upstream NOTICE text
changes no What we changed, so a reader can tell our edits from upstream’s

Each field is one line: // field: value. The header must come first, before the code. Only the first comment block counts. When a field is written twice, the first one wins.

yokoten check reads the header of every file in src/borrowed/, in the whole tree and in the commit about to be made. It reports each field that is missing, with the file and the line to add. A commit with a bad header does not pass. A source that is not a GitHub file link, or a commit that is not a full sha, is refused too. The watch reads only github.com/<owner>/<repo>/blob/..., so the check refuses any other host with a fix.

A license outside the five above is refused, even when the header is complete. The fix is use the clean-room road in record 0024. The same applies to a license name the check does not know.

The borrowed workflow runs yokoten borrowed every Monday. For each wheel, the command asks GitHub for the commits on upstream’s default branch that touched the source file. If a commit comes after the one we took, upstream has moved.

The command opens one issue per source, not one per file. It looks for an open issue that already names the source, so a later run adds nothing. If the host cannot list the open issues, the run stops with an error; it never opens a second issue. The issue lists the local files, the commit we took, and the newer upstream commits. A commit message that names a security fix gets the title check: take a borrowed security fix; other changes get check: review borrowed code upstream.

When files that share a source took different commits, the command compares each one. If the commit we took is not in the list the host gives, it compares dates instead of calling every listed commit newer: a snapshot commit that never touched the file opens no issue. When the host can give neither the list nor that date, the source is a gap.

A source the host cannot answer for is a gap, not a pass. The job says which source, and fails, so nobody reads silence as “upstream did not change”. To see the report without opening issues, run yokoten borrowed --dry-run.

When upstream fixes a bug, take the new commit: update source and commit, re-apply our changes, and let the tests pass. When the change does not touch the part we use, say so in the issue and close it. To stop using a wheel, delete the file. The way back is also the way out: the code was small, and the tests show what it must do (record 0014).

Some commands run another project’s program, as they run git or gh. yokoten copies none of its code and does not list it as a dependency. The version is pinned in the code.

Program Used by Version License Where it comes from
sandbox-runtime (srt), Anthropic yokoten desk, the fence 0.0.81, in src/agents/desk/fence/srt.ts Apache-2.0 srt on PATH at that version, else npx

Updating the rules

When yokoten learns a lesson, its core rules change. yokoten install updates the skills and agents on your machine. Your project’s AGENTS.md is your file, so it changes only when you ask. Once it has ## This project, init never touches it again.

doctor compares the part of AGENTS.md above ## This project with the core rules of the yokoten you run. When they differ, it reports core-rules, and a session hears it too.

yokoten update shows the change and writes nothing. It names the sections that changed, and lists each line it adds (+) or removes (-). yokoten update --write replaces the core part. Everything from ## This project down stays byte for byte.

A line you edited by hand in the core part shows as -, because the update replaces it. Move your own rules under ## This project first. Without that heading, yokoten can’t tell the core rules from yours. Then it says so, and compares and writes nothing.

The first line of AGENTS.md names the kit that wrote the core rules, as a comment that GitHub does not show: <!-- yokoten kit 0.1.0 -->. init writes it, and update --write sets the new version. When the project’s kit is newer than the yokoten you run, update refuses and writes nothing, so the rules never go back to an older kit. doctor then says to update yokoten itself, not the project. A file without that line compares by its text only.

The copy in .yokoten/ keeps the kit’s AGENTS.md and its lessons, so doctor in CI and the hooks compares with the kit your project vendored. To take newer rules, run update from an installed yokoten, the plugin or a clone. Run init again from there to refresh .yokoten/ too. Run from .yokoten/ itself, init keeps that copy as it is.

With the Claude Code plugin, run /yokoten:update. It runs the plugin’s own yokoten, shows the change, and writes only after you agree.

Learning from an incident

An incident is a lesson. yokoten learn turns it into a check that every project runs:

flowchart LR
  A[incident issue] --> B[yokoten learn]
  B --> C[check draft]
  C --> D[a person reviews it]
  D --> E[learn --ship: in the kit]
  E --> F[doctor in every project]

Give it the issue’s number, or its URL for an issue in another repo. It reads the issue through gh, with your own sign-in. It calls no AI model: the draft comes from the issue’s sections.

Draft field Comes from
id incident-<number>
what the title, after area:
why the “Why it matters” section. With none, “Why it got through” and a TODO(reviewer) to rewrite it
fix the “Fix” section

A field with no section in the issue gets a TODO(reviewer), never a guess. The draft also copies the issue’s sections into a comment, for the reviewer.

yokoten learn 138 prints the draft and writes nothing. yokoten learn 138 --write adds two files in lessons/incident-138/:

  • lesson.ts: the finding, and an empty findProblems for the check.
  • lesson.test.ts: a positive control. It feeds the check a known-bad case and expects a catch.

The test fails until a person writes the check. So a draft never passes on its own. --write adds only: it keeps a file that is already there.

findProblems gets the folder of a repo. It gives one finding for each place with the problem. It reads files only: no network, no writes. Keep it fast, because doctor runs it in every project.

When the review is done, ship the lesson. In a copy of yokoten, run yokoten learn --ship incident-138. It moves the lesson into a numbered group, for example kit/lessons/1/incident-138/, so no folder holds more than 7 entries: a group takes 5 lessons and 2 more groups, and a lesson goes in the first group with room. It adds a row to the lessons table, kit/lessons/README.md: the id, the folder, the incident, and the yokoten version. While a TODO(reviewer) is left or the test fails, it refuses and moves nothing. A test in yokoten checks the same for every lesson in the kit. It is a command, not a git mv, so the move, the table row and the test run are one step.

A project runs the lessons of the yokoten that runs its doctor. Each copy of yokoten reads its own kit/lessons/: nothing is downloaded or synced. So after the ship:

Who runs doctor in the project Has the lesson
This copy: node path/to/yokoten/bin/yokoten.ts doctor at once
This machine: yokoten doctor after node bin/yokoten.ts install from this copy
The project’s own copy: node .yokoten/bin/yokoten.ts doctor after init again from this copy; commit .yokoten/
Everyone else, through npm or the plugin when their yokoten updates, at the next release

learn --ship prints the same list. A lesson that finds the problem gives a note, a row of the doctor report:

Project:
  note        lesson incident-138: test/api.test.ts: a test calls a server on this machine and does not set NO_PROXY; set NO_PROXY and no_proxy to 127.0.0.1,localhost while the test runs, then put the old values back

The first time a project’s full doctor runs a new lesson, it says so once. A lesson that finds nothing says note a new check ran: ok (incident-138); doctor now runs it in this project. One that finds a problem says note new lesson: incident-138 (<what>); doctor now runs it in this project, then the problem. It keeps the lessons it has seen in .git/yokoten/state.json, so the file is never committed.

The session start hears a new lesson too, once, when it finds a problem here. It is one line, and it names the command that shows where: yokoten: a new lesson learned in another project found a problem here: <what>. Run `yokoten doctor` to see where. A later session, or the same one after a compaction, does not hear it again. The session runs each new lesson at one start only, and keeps which in the same file, so a later start spends no time on it. A problem that comes later is for full doctor to report.

A note never fails doctor. To stop a lesson in one project, add its id to “notNow” in yokoten.json, with the reason: "notNow": { "incident-138": "a setup file sets it" }.

Try the loop. In a clone of yokoten, run node scripts/demo/loop.ts. In about a second, it walks the whole loop in a temp folder: an incident, the draft, a refused ship, the review, the ship, and the note in a second project. It runs offline, with a fake gh, and removes the folder after. --keep keeps the folder and prints where it is. The real loop is the same commands, with your own issue and your own check, in a clone of yokoten.

Lessons for yokoten itself

Sometimes the problem is in yokoten, not in your project: a guard missed a case, or a message was hard to follow. Write it down where you found it:

yokoten learn --upstream "the guard let git push --tags through" --area hook --fix "match the flag too"

The note goes to ~/.config/yokoten/upstream/, on your machine and outside every repo. --area and --fix are optional; without --area, the part is unknown. Before it is kept, the note is cleaned:

  • the project’s name becomes <project>, in any spelling: secret-proj, secret_proj, SecretProj and secretproj alike. A name of under three letters is left alone, as it would hide too many words; add it to your private marks if it points at you
  • a path into the project starts with <project>, also when it goes through a symlink
  • your home folder becomes ~, also when a line cuts its path short
  • your name, emails, gh login, Linux and Windows user names, private hosts and private marks become placeholders, as in yokoten redact; an email address becomes <email>

A note with a known secret shape is not kept: a token the commit check knows, a password in a URL, password=, token= or api_key= with a value, a JSON web token, or an OpenAI key. A note that still holds an IP address or a private mark after the cleaning is not kept either. yokoten says which kind of detail it found, never the value. Nothing is sent anywhere. Without a home folder (an empty HOME), nothing is kept.

In the yokoten repo, the session start and doctor say how many notes wait: yokoten: 3 lessons from other projects wait to become issues: yokoten learn --upstream --list. Other repos never see this line. Open an issue for each note, then remove it with yokoten learn --upstream --done <id>.

Settings

yokoten.json at the repo root holds the project’s settings. Every key is optional. A list you set replaces the default list; it does not add to it.

Key Default What it does
layout.maxEntries 7 The most entries a folder may hold (core rule 10)
layout.exempt none Folders that may grow past it. See Folders that may grow
tests *.test.*, *.spec.*, *_test.go, test/**, tests/** The test files a fix task may not change
leadOnly the rules, agent config, .githooks/**, .github/**, the vendored runtime .yokoten/**, .yokotenignore The files a worker may not change
privacy.allow none Exact strings the privacy check lets through, such as a public support address, or this project’s own name from your private marks
privacy.skip lock files Files the privacy check does not read
privacy.utc true Commit times must be in UTC
privacy.output "run" In an agent other than Claude Code, a command that prints account details runs as it is. "stop" stops it and shows the filtered command
privacy.skipOwnName true A private mark that is this repo’s name is not looked for in this repo. false looks for it too
notNow none Steps not taken up yet, by doctor’s name, each with a reason: { "ci": "no remote yet" }
allowBuild none Build output and binaries the commit check lets through
areas any The areas a title may name. See Issue titles
translations.fail false A translation behind its source fails the check
worker.timeLimitMinutes 180 A worker that a runner or a desk starts stops after this many minutes
worker.settings none Environment settings a worker may put before a command, such as a test’s output file: ["SCREENS_OUT"]. A name that moves a folder or runs other code (PATH, HOME, GIT_*, LD_*, NODE_OPTIONS and the like) is a config error, and the list then gives no extra names

Folders that may grow

Core rule 10 keeps each folder at 7 entries or fewer. Two kinds of folder may grow past it, and changes/ and .worktrees/ always may:

  • A log, such as docs/records. Each entry is added once and kept.
  • A third-party fixture set that tests read, kept under its upstream names, with its NOTICE beside it. Tests, the NOTICE and upstream bug reports find a file by its name. Folders would make each lookup and each refresh from upstream harder.

List each such folder in layout.exempt by hand. An entry is a path from the root, or a glob. In a glob, * is one folder and ** is any number of folders, none too. No other character is special. One glob covers every crate of a Rust workspace:

{ "layout": { "exempt": ["docs/records", "crates/*/tests/data"] } }

check and doctor note a glob that matches no folder, so a stale entry shows.

Environment variables:

Variable What it does
YOKOTEN_ROLE=worker The session is a worker. It may not change lead-only files
YOKOTEN_DONE_WHEN The command the stop hook runs when this worker ends the task. It wins over yokoten-done-when in the worktree’s git folder. A subagent shares the lead’s environment and does not get its own. See The stop check
YOKOTEN_LEAD_ONLY_OK The lead-only paths this worker may change, comma-separated, from the repo root. See Works with
YOKOTEN_FIX_TASK=1 The session is a fix task. It may not change test files
YOKOTEN_RELEASE_APPROVAL=1 The owner allows a release from this session. Muse Code is not known to pass it to a hook
YOKOTEN_PRIVATE_MARKS A second private-marks file. Its marks are added to the ones in ~/.config/yokoten/private-marks; it never takes that file’s place. See Privacy
YOKOTEN_MIN_FREE_GB doctor notes a disk with less free space than this, in GB. 0 turns the note off. See Machine notes
YOKOTEN_BOT_APP_ID, YOKOTEN_BOT_KEY, YOKOTEN_BOT_INSTALLATION The bot. They can also be lines in ~/.config/yokoten/keys.env. See server.md
YOKOTEN_GITHUB_API Another GitHub API address, such as GitHub Enterprise Server
YOKOTEN_PR_LIVE=1 check --pr reads the pull request’s title, body, labels and milestone as they are now, with GITHUB_TOKEN, not from the event file. It fails when it can’t read them. The kit’s pr.yml sets it
LLAMA_API_KEY or MODEL_API_KEY, LLAMA_API_BASE, LLAMA_MODEL The model for adopt. See A first draft of the table