Personal agent config shared by Claude Code and Codex, versioned in git. It runs on Linux and Windows, both tested in CI; Windows lists its setup and the Linux-only parts. macOS is not supported.
install.py symlinks chosen parts of this checkout into ~/.claude, ~/.codex and ~/.local/bin, or a project's .claude
instructions/ user.md, the user-wide instructions
CLAUDE.md instructions for work in this repository
AGENTS.md -> CLAUDE.md, for Codex
skills/ skills for both agents
agents/ <name>.md for both agents, plus <name>.toml for Codex
hooks/ Claude Code hooks, one folder each with a hooks.json
workflows/ saved workflows (Claude only)
bin/ commands for the human, such as lsusbt
statusline/ the Claude Code status line, its Codex usage fetcher and its session-PR helper
lib/ helpers the scripts share through their resolved path: windows.py (Windows runtime), greptile.py
tests/ unit tests for skill scripts, hooks and the installer
bench/ benchmark corpora and their runners; data too large for git is a test-data release asset
Every entry is a symlink, so edits are live. Each flag takes every entry of its kind; nothing is installed by default. For use in a project, install into its work tree (Per project):
git clone git@github.com:hathach/agentrc.git ~/code/agentrc
~/code/agentrc/install.py install --project ~/code/tinyusb --skill --agent --workflowThe user-wide install is the owner's personal mode; its --claude-md makes
instructions/user.md, the owner's own rules (hosts, sudo, document library),
every session's instructions:
~/code/agentrc/install.py install --skill --agent --workflow --bin --claude-md --statusline
~/code/agentrc/install.py remove --skill--skill: into~/.claude/skillsand~/.codex/skills. A skill's hook of the same name links into~/.claude/hooksand its events are registered in~/.claude/settings.json(backed up the first time).--agent: the.mdinto~/.claude/agentsand~/.codex/agents, the.toml, if any, into~/.codex/agents.--workflow: into~/.claude/workflows.--bin: eachbin/command into~/.local/bin, which must be onPATH.lsusbtislsusb -twith device names, serials and the HIL board each serial belongs to; its header lists the options.--claude-md:instructions/user.mdas~/.claude/CLAUDE.md, and~/.codex/AGENTS.mdto it.--statusline: thestatusline/files into~/.claude, andstatusLinein~/.claude/settings.json;installrefuses if another one is set. The command names$HOME/.claude, so anotherCLAUDE_CONFIG_DIRcan reuse it.
The target directories stay real, so local entries sit beside the links.
install refuses before touching anything if a target is a file or a
nonempty directory; remove never deletes one, and leaves CLAUDE.md links
that point elsewhere. Rerun after adding an entry: dead links into this repo
are pruned.
On a host that ran the former install.py install --herdr-plugin, run herdr plugin unlink agentrc.worktree-children
once to drop herdr's registration of the removed plugin.
Project repos such as tinyusb call these skills by bare name and expect this install, user-wide or per project.
--project <dir> installs the Claude Code entries into one work tree instead,
and touches nothing under ~/.claude, ~/.codex or ~/.local:
~/code/agentrc/install.py install --project ~/code/tinyusb --skill --agent --workflow
~/code/agentrc/install.py install --project ~/code/tinyusb/.worktrees/<branch> --skill --agent --workflow
~/code/agentrc/install.py remove --project ~/code/tinyusb --skill --agent --workflow<dir>must be a repository's or a linked worktree's top level. Claude Code finds project skills, agents and workflows only up to the work tree root, and reads project settings, hooks andenvincluded, only from the directory the session starts in, so install every worktree a session starts in, and start sessions at its top level.- Skills, a skill's hook, agents (the
.mdonly) and workflows link into<dir>/.claude/{skills,hooks,agents,workflows}. The hooks are registered in<dir>/.claude/settings.local.json, never the trackedsettings.json, by absolute path as user-wide, withenv.AGENTRC_DIR, this checkout. The agentrc paths the agents, workflows and skill recipes name are"${AGENTRC_DIR:-$HOME/.claude}/..."($HOME/.codexfor Codex), so they resolve to this checkout in a project install and to the user-wide links otherwise; a model that reads one by path expands it in the shell first. A skill names a sibling skill's page relative to its own directory, its symlink resolved, which reaches this checkout either way.captainis the exception (below). --claude-mdis opt-in: it linksinstructions/user.md, the owner's personal rules, as<dir>/CLAUDE.local.md, Claude Code's per-user instructions for that project, so those rules and the co-words hold in that work tree only. A link, because an@-import of a file outside the project loads only once approved interactively, never in a headless session.- Each link, and
settings.local.jsonunless git already ignores it, gets a line in one marked block of the repository's.git/info/exclude, which its worktrees share; a line stays while any worktree has our link there, sogit statusstays clean everywhere.removedrops the links, our hooks andAGENTRC_DIR(once no link of ours is left), the file once it is empty, and the lines no worktree needs. Since the block is shared, a project's own untracked file at the path of our link in another worktree is hidden fromgit statustoo. - A destination the repository tracks, or a real file or nonempty directory
there, such as the project's own skill of the same name, refuses before any
change;
--skip-clashesleaves it and skips that agentrc entry (and the hook registration with a skipped hook, and a skipped skill's hook, whose link and registration from an earlier install go, unless every selected entry is skipped: theninstallchanges nothing). A directory left with only__pycache__from a skill the project has since moved out is named as such; delete it. Pruning leaves a tracked link alone. - Both actions refuse a symlinked directory between
<dir>and a destination,.claudeitself included, which would carry them out of the work tree, and asettings.local.jsonthat is tracked or a link, since they rewrite it. - Project mode is Claude Code only:
--binand--statuslineare user-wide and refused with--project, and Codex's own skills and agents have no project install. A Codex coworker needs noCLAUDE.local.md:coworksends it its rules. - A user-wide install on the same host is not supported beside a project
install: the hooks are registered twice and race, and the
headless-chiefshadow check misjudges links into a different checkout. A project install leaves a user-wide one in place; remove it as its own step,install.py removewith the user-wide flags and no--project. - Not supported with a project install either:
captain, whose guard and scripts are user-wide only;- worktrees of one repository installed from different agentrc checkouts;
installorremoveruns at the same time in worktrees of one repository;bench/finding-verifyreplay fingerprints, which read the user-wide install.
Claude Code on Windows runs the Bash tool, hooks and the status line through
Git Bash (or MSYS2), and the scripts run on native Windows from there. They read
and write UTF-8 themselves, so no -X utf8 or PYTHONUTF8 is needed. Bash's
PATH needs:
- a
python3that is a native CPython 3.12+ (python.org's install manager or MSYS2's mingw build), not the WindowsApps alias and not MSYS2's/usr/bin/python3, for the workflows' script calls and the simplify gate's wrapper; PyYAML, pytest and pytest-xdist in it for the tests, and PyYAML forpr-babysit'sharvest.py, whichpr-reviewalso imports, to date a head by its pull_request runs (without it they go unread and the harvest says so).install.pyregisters a Python hook as'<the python that ran install.py>' '<hook>.py', since a hook that cannot start lets the tool call through; rerun it after replacing that Python, and name the interpreter the same way in the credit guard's recipe below. bash,cygpathandgitfrom Git for Windows or MSYS2: from Python a barebashis System32's WSL launcher, so scripts look it up onPATH.chief_guard.pymaps/tmp-style paths throughcygpathand denies them without it; it also deniescmd, PowerShell andwsloutright.- poppler's
pdfinfoandpdftotextforread-doc(MSYS2mingw-w64-x86_64-poppler);pdftotextis taken from besidepdfinfo, so Git's xpdf earlier onPATHdoes not matter.
Claude Code sets NoDefaultCurrentDirectoryInExePath; set it too when running
the scripts from another shell, or Windows runs a git.exe or gh.exe from the
current directory, a PR checkout for many of them.
pr-babysit's build check puts a temp directory into the build command
unquoted, so it refuses a temp dir with a blank or shell syntax in it, as a
profile path such as C:\Users\First Last gives; point TMP and TEMP at a
plain path such as C:\tmp there.
install.py creates symlinks, which needs Developer Mode or an elevated shell.
Not on Windows yet; #148 has
the skipped tests and notes for a port:
hil's rig-host scripts (hil_lock.py,hil_reserve.py,usb_observe.py,hil_process.py,hil_farm_check.py,hil_tt.py,hil_usbtest.py) andusb-kernel-recover'susb_recover.sh: POSIXflockandfork,/procand sysfs.hil'shil_report_notice.pylocks the report writer withmsvcrton Windows, but its tests andhil_report_core.py's usefcntland skip there.lsusbt: sysfs.usb-kernel-debug: kernel dynamic debug and usbmon, which have no Windows counterpart to port to.- the capture scripts of
usb-sniffer(sniff.py) andtarget-debug(etm_capture.py,dual_capture.py): usbfs,/procand POSIX process control. target-debug'ssysview_record.py, which refuses there: it drives SystemView under Xvfb and tracks its processes through/procand process groups.target-debug'sesp_session.py, which refuses there: it detaches openocd as a POSIX session and identifies it through/proc(#130).
The hardware-debug scripts run on Windows but are unverified on a board there
(#130), and their tests skip the cases that drive POSIX stub executables or
ptys. Other skips are for /proc, permission bits or file names NTFS refuses.
Agent collaboration and PRs:
| Skill | Use it to | Start with |
|---|---|---|
cowork |
hand the other agent (Codex or Claude) a task, question or review headless, in resumed lanes | cowork.py send --lane L --task - |
simplify-gate |
switch the per-repository Codex YAGNI check at Stop (below) | /simplify-gate status|on|off |
pr-reply |
post PR review replies from a manifest, read each back, resolve verified review threads | reply.py --pr N --manifest f.json |
followup-issue |
open a follow-up issue, or comment new evidence on one that covers the topic, read it back, never post twice | publish.py create --repo o/r --allow-repo o/r --title t --body-file f |
pr-babysit |
the fact collectors the pr-babysit workflow runs before it commits a fix | preflight.py --pr N, hooks.py <path>... |
pr-review |
review someone else's PR in one request, such as /pr-review N auto-post boards: <ids>: pin it, run the pr-review workflow on the boards named (none by default), leave a pending review for you to submit (or submit it under auto-post); re-review after a push |
prepare.py --pr N |
ci-rerun |
read a failed CircleCI job's or Read the Docs build's log, rerun it once after an infra failure | circleci.py log <job>, rerun <job>..., rtd.py log <url> |
headless-chief |
launch a headless chief and follow its status lines while it runs, its report when it exits |
chief_run.py --out <dir> --worktree <wt> --task-file <task> |
worktree-remove |
remove a worktree when its task is done, run from outside it: check it, gate on pending follow-ups and PR state, close the hc terminals working in it when run from one, then git removes it and scripts/worktree_children.py its clean <name>--* siblings and its branches whose commits are on their remote or a PR head |
worktree_remove.py [PATH] --dry-run [--pending <item>], --proceed <token> |
Hardware documentation, from the Calibre library at ~/Documents/calibre-library
(CALIBRE_LIBRARY overrides):
| Skill | Use it to | Start with |
|---|---|---|
read-doc |
look up hardware manuals, specs and errata in Calibre instead of answering from memory | search.py --kind reference-manual stm32h7 |
download-doc |
fetch vendor datasheets, manuals and errata into the library, refresh stale revisions | sync.py st --family STM32H7 --types datasheet,errata (dry run; --apply imports) |
Board designs, from KiCad schematics in hathach/pcb, or EAGLE and KiCad
schematics in a directory you name:
| Skill | Use it to | Start with |
|---|---|---|
read-pcb |
answer board-wiring questions (which pin, net, part or revision) from the schematic | pcb.py find --in <dir> <board>, then net <sch> <net> |
Firmware footprint; the project supplies the launcher, its builds and its CI
baseline through the Code size section of its Build contract::
| Skill | Use it to | Start with |
|---|---|---|
code-size |
measure flash/RAM per board, file, section or symbol, and diff it against a base ref or CI's stored snapshots | <launcher> diff -b <board> |
Firmware and USB debugging on real hardware; target-debug maps which one
answers what, and the project supplies the build variant and board locks
through its Build contract: and HIL contract: lines:
| Skill | Use it to | Start with |
|---|---|---|
hil |
run a project's HIL tests and judge their results: contract sections, board locks, verdicts, retries; a farm check (each board's probe on the bus and resetting it), never a test verdict | the project's HIL contract: page |
arduino |
the mechanics an Arduino library's build and HIL contracts share: arduino-cli builds against its checkout, prepared dependencies, the HIL build record, the harness and its remote runs; how a further library adopts them | the library's Build contract: and HIL contract: pages |
hil-farm-setup |
provision or update a Debian 13 HIL farm host: CI runners, pinned flashing tools, udev rules, runner firewall, from a root-owned checkout and the host's file in hosts/ |
setup.sh install --host hosts/<host>.toml --inputs DIR, then verify.sh |
target-debug |
explain what the firmware did: GDB autopsy, faults, RAM trace, PC sampling, SWO, ETM instruction trace (J-Trace + Ozone), SEGGER SystemView scheduling profiles; Espressif chips over USB-Serial-JTAG or pad JTAG with openocd-esp32 (read its espressif.md first) |
pc_sample.py --probe <uid> --device <dev> --interface swd --speed 4000 --elf <elf> |
rtt |
console or log over a debug probe (SEGGER RTT), live or post-mortem | rtt.py --backend jlink --probe <serial> --device <dev> --interface swd --speed auto --seconds 20 |
usb-kernel-debug |
capture Linux host URBs (usbmon); kernel dynamic debug on a host or gadget | usbcap.py <vid:pid> 10; sudo usb_dyndbg.sh on|off usbcore xhci_hcd |
usb-sniffer |
what crossed D+/D- with the ataradov sniffer, where usbmon cannot see | sniff.py capture raw.pcapng --port <port> --seconds 30 |
hooks/simplify-gate snapshots the checkout, the worktrees nested in it and its <checkout>--* sibling worktrees
(linked worktrees of any repository; the user rules reserve that prefix for them) when a prompt
arrives and when the session stops, then sends the diff to a read-only
codex exec YAGNI challenge at Stop: at most two rounds per user turn, one
retry on Codex failure, then the stop goes through with a notice. One review
runs at a time; edits it did not cover wait for the next turn. The challenge notes that
a peer sharing the checkout may have made part of the diff, and Claude rejects
findings on files it neither wrote nor commissioned.
--skill registers the hook; switch the gate on per repository with
/simplify-gate on in a Claude session there, or:
cd ~/code/tinyusb && ~/code/agentrc/skills/simplify-gate/scripts/gate.py onThe hook costs one git rev-parse per checkout and starts the gate only where
<git common dir>/simplify-gate exists, so one marker covers a repository and
its worktrees. /simplify-gate status prints the state with the effective
model and effort; on --model M --effort E overrides the defaults at the top
of simplify_gate.py. Session state lives under
~/.cache/agentrc/simplify-gate/.
hooks/credit-guard keeps a session on an account with extra usage enabled
from spending credits. At session start it asks /api/oauth/usage once
whether credits are enabled; if not, it stays off for that session. Otherwise
it blocks the prompt, or denies the next tool call and ends the session, once
any plan limit reaches 100%. Usage is re-read on a window that shrinks with
headroom and burn rate (HEADROOM in credit_guard.py, tuned from
credit-guard/readings.log); a read that fails, or a payload it cannot
parse, keeps the last reading, and blocks only when there is none. It cannot
stop a model call already running, a subagent's next turn, or a hook that
Claude Code kills on timeout, and it does not know the session's model.
install.py does not register it. Register it per config dir that needs it
(~/.claude-ada here); the recipe refuses a dir that already has it:
d=~/.claude-ada; ! grep -q credit_guard.py $d/settings.json && mkdir -p $d/hooks &&
ln -sfn ~/code/agentrc/hooks/credit-guard $d/hooks/credit-guard &&
jq --arg c "$d/hooks/credit-guard/credit_guard.py" --slurpfile h ~/code/agentrc/hooks/credit-guard/hooks.json \
'.hooks = reduce ($h[0] | to_entries[]) as $e (.hooks // {}; .[$e.key] += [$e.value[] | .hooks |= map(.command = $c)])' \
$d/settings.json > $d/settings.json.new && chmod --reference=$d/settings.json $d/settings.json.new &&
mv $d/settings.json.new $d/settings.jsonagents/chief.md is the orchestrating main session: it reads and runs short
chores itself, and every worktree edit, long build or investigation and review goes
to the repository's agents, skills and workflows. hooks/headless-chief/chief_guard.py
denies chief's own direct git/gh writes, workflow-only publishers and writes
into its worktree, a guard against mistakes, not isolation: it judges only the command
families it knows, and a program outside them passes by design, even one that runs its
arguments as a command. Its direct Codex exchanges go through agents/coworker.md, the
cowork.py transport. workflows/code-audit.js is its saved review: one code-verifier
per directory x dimension, then finding-verifier on every finding (on Sonnet for a
scanner-labelled nit)
(args: { dirs, dimensions, diff? }, the first two required; diff: { base, head }
narrows it to a change); the verifier sets each finding's level by the one
severity scale in agents/finding-verifier.md's Severity section.
workflows/pr-review.js reviews a pinned PR head through it. Start it inside the task
worktree:
~/code/agentrc/install.py install --agent --workflow --skill
git worktree add .worktrees/<branch> -b <branch> <base> && cd .worktrees/<branch> && claude --agent chiefFrom a linked checkout the task worktree is its sibling <checkout>--<branch>
instead (instructions/user.md, linked checkouts).
Headless, launch it through the headless-chief
skill, which runs claude -p --agent chief and gives the caller chief's status
lines while it runs and its report when it exits.
A branch whose .claude/workflows or .claude/agents entries differ from the
same-named agentrc ones installed would run them in their place, since project
scope wins; merge the default branch first. The launcher refuses such a
worktree, and the headless-chief hook (--skill registers it) denies every
Workflow and Agent call in it, interactive sessions included; the chief
prompt itself loads before any tool call and is not covered.
For headless PR publishing, follow agents/chief.md's Authorization exception:
the human's request to launch chief to babysit a PR, their yes to the offer, or
their request to open a new PR under the user-wide rule, is the grant, with no
further question. Include that message verbatim (with the offer it answered
when it is a yes, and any later restriction), the PR resolved from it
(repository, PR URL, head repository and branch, expected HEAD, worktree) and
the task scope, and, when a pr-babysit launch continues an
earlier chief's run, that run's last reported stateRef. The task scope names
the review scope: the whole PR, or takeover-only (earlier PR commits are context,
the takeover HEAD is the task base) for a task limited to the repairs it
commissions; an offer to launch names that choice when the human has not, and an
unspecified review scope defaults to the whole PR; ask only when the PR,
repository or worktree is ambiguous. A relaunch for the same PR task copies
the grant verbatim from the earlier launch task, without asking again, until
the task is done, the PR changes, or the human narrows or withdraws it; a
later restriction rides along verbatim. A merge is granted only by the
human's own words asking to merge that PR, under chief's merge exception: the
task then also names the base branch and carries Merge grant: yes — "<the words>", else Merge grant: none, a line a relaunch copies too. A PR review
submits under its own PR review exception, as skills/pr-review/SKILL.md asks
it, an auto-fix review with the babysitting of its repairs; its pending reviews
need no grant, under chief's standing exception.
Leave the checkout to chief until it exits.
agents/captain.md is one seat over the other Claude Code sessions in this host's
hc terminals (Codex terminals are left out): it lists them, opens new ones on existing
checkouts under either account, asks the idle ones for status, reports one table
and carries the human's own words, read from its own transcript, to the sessions
they name. It checks what sessions report with read-only git and gh lookups and
the files sessions and chiefs leave, and keeps its own memory, but never does the
sessions' work; the one thing it publishes is a follow-up issue in hathach/agentrc
for a defect in its own tooling, on the human's word. Its guard,
skills/captain/scripts/captain_guard.py, declared as a hook in the agent file so
it applies on every account, names in its docstring the calls it allows. It reads
sessions from hc terminals, each pinned to the session Claude Code registered under
its process's account config dir, because sessions run under more than one account
(CLAUDE_CONFIG_DIR), each shown with the account its process runs under. Start
it in its own terminal:
~/code/agentrc/install.py install --agent --skill
claude --agent captainA watch has sessions report at the end of each task, new and restarted ones too, until the captain session that
started it ends. It types into no session: hooks/captain, installed with the captain skill, lets a covered session
join it at its next Stop or after a compaction or /clear, and costs a Python start and one marker-file check while
no watch is open. Another config dir (~/.claude-ada here) needs the hook's two entries from
hooks/captain/hooks.json in its own settings.json, as the credit-guard recipe above does for its own; a session
started before its settings held them joins only once restarted.
Its memory is shared across accounts: an account whose config directory is not
~/.claude links its agent-memory there, as the ada account does with
ln -s ~/.claude/agent-memory ~/.claude-ada/agent-memory; without that link the
guard denies its memory writes.
python -X utf8 -m pytest -n auto --dist worksteal -p no:cacheprovider -q testsNeeds PyYAML, pytest and pytest-xdist; BUILD.md is the full contract. The pre-commit hook
(pre-commit install) runs the same command with -n 8, well inside pr-babysit's 540 s hook
deadline; CI shards it across jobs (.github/workflows/tests.yml), and --shard I/K
(tests/conftest.py) runs one part of it.
Where Git for Windows has core.fsmonitor=true (an installer option), each
temporary test repository leaves a git fsmonitor--daemon behind; run the suite
with GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0=core.fsmonitor GIT_CONFIG_VALUE_0=false.