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, thencheck --pr.issues:check, thencheck --issueon the issue’s title. To run it, addissues:withtypes: [opened, edited]underon:.
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
@mainchanges under you. Once there is a release, a release tag works too. - The checkout needs
fetch-depth: 0.check --prlists the files between the base and the head commit, so it needs both. - Permissions:
contents: readis enough. The action takes no token. - Inputs:
pr-check(defaulttrue),issue-check(defaulttrue) andnode-version(default^22.23.4, the first Node 22 with the October 2026 security fixes). Setpr-check: falseto skip the pull request standard,issue-check: falseto 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"inyokoten.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
initnames 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
mainunchecked.
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.ymlupdates the actions that CI runs, and the packagesinitfinds (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;initnames the packages it does not update.SECURITY.mdasks people to report a vulnerability privately, not in a public issue. For this, turn on private vulnerability reporting.doctorlists it with the other security switches.CODEOWNERSnames 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 GitHuboriginremote. Without one, the file has a placeholder: make the repo, add the remote and push, thendoctorasks you to put your GitHub handle in it. GitHub takes no organization as an owner, so whenghsays the account is one, the file has the placeholder too, andinitasks for a person or@org/team. Fulldoctorflags 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 indocs/, 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 }.checkistree,staged,history,prorissue. A note never fails the check. When the CI run has no pull request or issue,check --prandcheck --issueare ok and addskipped, the reason.doctor --json:{ version, ok, selfTest, repo, stage, stages, next, findings, steps, notes, server, lessons }.selfTestis{ ok, failed, ran, skipped, reason? }:okmeans no check failed;ranis false, withskippedandreason, when a control could not make a temp folder and did not run. The findings of a lesson are innotes; a new lesson hasisNew: true. With--session, only the next step, the private-marks finding, andcore-ruleswhen the rules are behind. Thennotesholds 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_activeaddsskipped, the reason. A worker is still checked when that field is set. When the check itself fails,skippednames that too. When a worker’s done-when check fails, alsodoneWhen. After the try limit,skippedisstopped with failing checks. That stop writeschecksasfailingin.yokoten-agent.jsonin a linked worktree. A pass there setscheckstopassing. 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_GBto use another limit, or0to 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,
dfshows the size of the disk file, not the free space on that drive. Fulldoctorreads the drive throughreg.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. Runyokoten doctorto read the drive again. WithoutWSL_DISTRO_NAME, as undersudo, 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 inCLAUDE_CODE_TMPDIR, else inTMPDIR, 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 printssudo 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. Fulldoctorcounts them. It does not add up their size, so it stays fast. The note prints afind ... -exec rm -rfcommand 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
doctoronly, 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.coworkerandcode-workerare not workers. Mark such an agent asworker, 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 fromyokoten-done-whenin 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
shon Linux and macOS, andcmd.exeon 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.exeruns in a job when Windows PowerShell is there. Closing the job stops those programs aftercmd.exehas 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 blockAdd-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 runscmd.exealone 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.
setsidstarts one. A new process group in the same session is stopped on Linux. On macOS,psgives 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.jsonat the root of a linked worktree. It does not write that file in the main checkout. It setscheckstofailingandupdatedto the time in UTC. A pass setscheckstopassing. Other fields stay. The lead reads that file. Its/api/stateshows 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
timeoutin 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 readtimeoutin 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 inprivacy.skipinyokoten.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/python3andpython3.execount aspython3. - 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 asenv,sudo,nice,timeout,xargs,setsidandstdbuf; a command afterif,for,whileor!; a programfind -execruns; and the string inenv -S. - A word that only holds the name passes:
echo python,cat python.md,pythonic, and acasepattern such aspython3).command -v python3passes 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_DIRor$(pwd). Name the folder with a plain path instead. - A disk tool is refused even when it only reads:
fdisk -l,parted -l,diskutil listandgpt -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.checkanddoctorexit 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.jsonis not valid JSON, or is not one JSON object,checkanddoctorreport 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 exampledoctor: add the review step. - An incident:
area: symptom, for examplecheck: 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,docsand the like) and one tier label (tier A,tier Bortier 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 emptyfindProblemsfor 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,SecretProjandsecretprojalike. 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
NOTICEbeside it. Tests, theNOTICEand 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 |