CLI Commands

The agentihooks CLI is installed globally via uv tool install --editable . as part of agentihooks init. All subcommands are idempotent.

Table of contents

  1. agentihooks init
    1. What it does
    2. Flags
    3. Environment variables
    4. Examples
  2. agentihooks deps
  3. agentihooks settings-profile
    1. Environment variable
    2. Examples
  4. agentihooks broadcast
    1. Severity behavior
    2. agentihooks broadcast emit
    3. Examples
  5. agentihooks channel
    1. Subcommands
    2. Subscriptions live in settings.json, not in CLI
    3. Examples
  6. agentihooks enforcement
  7. agentihooks ledger task split
  8. agentihooks conditions
  9. agentihooks recall
  10. agentihooks knowledge
  11. agentihooks claude / agenti
  12. agentihooks balance
  13. agentihooks init-agent
  14. agentihooks select-profile
  15. agentihooks headless run
  16. agentihooks terminate-agent
    1. Terminate a process
    2. Stand down one Codex conversation
  17. agentihooks herdr
  18. agentihooks quota
  19. agentihooks classify, classifier stats, classifier eval
  20. agentihooks balance
  21. agentihooks gc, lease, scratch
    1. Git maintenance and object counts
      1. Configure discovery
      2. Read a checkout’s backlog
      3. Troubleshooting
    2. Worktree verdicts
    3. Lease holders and resumed agents
    4. lifecycle.json
  22. agentihooks refresh-rules
    1. How it works
    2. Flags
    3. Examples
    4. Troubleshooting
  23. agentihooks mcp
    1. The daemon subcommands
    2. status and its exit codes
    3. Supervisors
  24. agentihooks uninstall
    1. What gets removed
    2. What is NOT removed
    3. Flags
  25. agentihooks claude
    1. How it works
    2. Examples
  26. agentihooks ignore
    1. What it creates
    2. Flags
    3. Examples
  27. agentihooks --list-profiles
  28. agentihooks bundle
    1. Subcommands
    2. Slots
    3. Examples
  29. agentihooks link-profile
    1. Subcommands
    2. Flags
    3. Examples
    4. State
    5. Stale paths
  30. agentihooks --query
  31. agentihooks status
    1. What it checks
    2. MCP fleet introspection
    3. In-session skill
  32. agentihooks lint-claude
    1. Output
  33. agentihooks extract-skill
    1. Flags
  34. Standalone Python execution
  35. Exit codes
  36. agentihooks swarm wait prerequisite merges

agentihooks init

The single entry point for installing agentihooks. Handles global setup and bundle linking.

Plain agentihooks init installs Claude Code and Codex. Use --target claude or --target codex to install one supported harness.

Default output shows the linked bundle, installed profile, resolved profile chain and overlays, then each harness home and installed MCP server, skill and plugin counts. Stored swarm role profiles are never recalled for an operator home; Codex borrows the operator profile from Claude instead. Actionable warnings and errors remain visible. Use --verbose for installation details, kept MCP notices and launch hints.

agentihooks init [--bundle <path>] [--profile <name>]

What it does

  1. Links bundle directory (if --bundle is provided)
  2. Merges settings in the target’s own format: _base/<target base> -> bundle .<target>/ -> profile .<target>/ overrides
  3. Substitutes /app -> real repo path and __PYTHON__ -> venv Python in all commands
  4. Preserves personal keys (model, autoUpdatesChannel, skipDangerousModePermissionPrompt) from any pre-existing unmanaged settings
  5. Writes ~/.claude/settings.json with hook wiring and tool permissions
  6. Symlinks skills, agents, commands, and rules via 3-layer merge (agentihooks built-in -> bundle global -> each profile in chain)
  7. Writes ~/.claude/CLAUDE.md – single profile: file copy; chained profiles: concatenated with --- separators and <!-- profile: name --> markers
  8. Installs MCPs (agentihooks + bundle .claude/.mcp.json + profile .claude/.mcp.json)
  9. Installs the agentihooks CLI globally via uv tool install --editable .
  10. Writes managed bashrc block (agentienv function + agenti alias)

Step 8 has one extra behaviour when MCP_TRANSPORT names a network transport: it renders the systemd unit and starts the agentihooks daemon, restarting it unconditionally. Reverting to stdio stops the daemon and removes the unit, so a downgrade cannot leave a process serving a port nothing points at. See agentihooks mcp and MCP Transport.

Per-repo init (--repo / --local / .agentihooks.json) was removed 2026-05-07. agentihooks init is global-only.

Flags

Flag Description
--bundle <path> Path to bundle directory. First-time: links the bundle and runs global install.
--user-bundle <path> Path to the personal user bundle. Linked beside the root bundle, which stays linked; kept by --force. AGENTIHOOKS_USER_BUNDLE_PATH names one when none is linked.
--target claude\|codex Install only the selected supported harness.
--verbose, -v Show all installer output, including linked skills and configuration details.
--profile <name> Profile to install. Comma-separated for chaining: --profile coding,anton (default: default, env: AGENTIHOOKS_PROFILE)
--force Clean install — resets install state (state.json, sync hashes, session caches, PID files, prod_bypass/, controls_flags/, voice_flags/, force_refresh/) and re-symlinks ~/.claude/ assets. Preserves broadcasts, enforcements, brain data, logs, quota accounts, .env, .venv.

Environment variables

Variable Description
AGENTIHOOKS_PROFILE Default profile when --profile is not passed (default: default)
AGENTIHOOKS_SETTINGS_PROFILE Default settings-only overlay profile (default: none)
AGENTIHOOKS_MCP_FILE Path to an MCP JSON file to auto-merge into ~/.claude.json during install
CLAUDE_CODE_HOME_DIR Home-directory root override – .claude is appended automatically (default: $HOME)
AGENTIHOOKS_CLAUDE_HOME Legacy: direct path to the .claude directory (default: ~/.claude)
AGENTIHOOKS_HOME Override the agentihooks state directory (default: ~/.agentihooks). Used for per-pod isolation on shared filesystems — set to /shared/.agentihooks-<pod-name> so each pod gets its own state without racing on state.json.
AGENTIHOOKS_MCP_TRANSPORT One-off override of MCP_TRANSPORT for a single init, without editing .env.
AGENTIHOOKS_MCP_SUPERVISOR auto (default), systemd, or pidfile. Forces the daemon backend instead of probing for a systemd user bus.

Examples

# First-time install with bundle
agentihooks init --bundle ~/dev/my-tools --profile coding

# Re-run global install (uses linked bundle)
agentihooks init

# Install with a different profile
agentihooks init --profile admin

# Clean install (fresh state, preserves .env)
agentihooks init --force --profile coding

# Install with persona + settings overlay
agentihooks init --profile anton --settings-profile admin

# Quick-switch settings layer only (keeps persona intact)
agentihooks settings-profile admin

# Revert settings to persona defaults
agentihooks settings-profile --clear

# Same, using the environment variable
AGENTIHOOKS_PROFILE=coding agentihooks init

# Auto-merge a gateway MCP file during install
AGENTIHOOKS_MCP_FILE=/shared/gateway-mcp.json agentihooks init

agentihooks deps

Inspect and maintain the running CLI’s declared Python dependencies and the linked bundle’s development tools.

agentihooks deps check
agentihooks deps sync
agentihooks deps ensure [--quiet]
agentihooks deps mark-changed --reason "MCP registration changed"
Action Behavior
check Report dangling skill links, missing declared Python distributions and unsatisfied bundle tools; exit 1 if any are missing, otherwise 0. Python reports include a repair command using the current interpreter.
sync Require the CLI’s own source checkout to be on dev, fast forward from origin/dev, install added or changed runtime requirements plus missing distributions into the running Python environment, and report installed specs. Exit 1 on a refused branch, failed pull or failed install.
ensure Repair skill links and refresh the Codex hook wrapper; on an uncached bundle preflight, attempt missing declared Python requirements and bundle tool repairs. Required missing system tools need operator intervention and return status 2. Declared Python install errors are reported while preflight continues.
mark-changed Record a session-affecting dependency change so managed sessions restart at their next stop. --reason defaults to manual; this action does not install packages.

--quiet suppresses installation messages and declared Python install errors from ensure; it does not turn sync or check output off. sync chooses its source checkout from the running code, not the shell’s working directory. It installs with uv pip install --python using the current interpreter, so invoke the installed CLI to update its tool environment.

Every SessionStart hook also runs ensure --quiet when the dependency stamp is older than six hours, waits up to one second, then kills the check’s process group. See Session start dependency check.

For examples, requirement declarations, Doctor integration, preflight caching, version-check limitations and recovery after a pull succeeds but installation fails, see Synchronize an editable tool installation.


agentihooks settings-profile

Quick-switch the settings layer without touching persona (rules, CLAUDE.md, skills, agents, commands).

agentihooks settings-profile [NAME] [--clear]
Argument / Flag Description
NAME Settings profile to apply. Only its settings/MCP files for the active target are used, not its persona or content.
--clear Remove the settings overlay and revert to persona profile defaults.

With no arguments, shows the current persona and settings profile.

Environment variable

export AGENTIHOOKS_SETTINGS_PROFILE=admin
agentihooks init --profile anton   # automatically uses admin settings overlay

Examples

# Show current state
agentihooks settings-profile

# Switch to admin settings (keeps anton persona)
agentihooks settings-profile admin

# Revert to persona defaults
agentihooks settings-profile --clear

agentihooks broadcast

Send a message to Claude Code and Codex sessions sharing the broadcast store.

agentihooks broadcast [OPTIONS] MESSAGE
Flag Default Description
-s, --severity alert critical, alert, or info
-t, --ttl per severity Time-to-live: 5m, 30m, 1h, 8h, 24h
--persistent per severity Re-inject on every hook event until TTL expires
--source operator Source tag: operator, system, cron, api
--list   Show all active broadcasts
--clear [ID]   Clear all broadcasts, or a specific one by ID

Severity behavior

Severity Injection Default TTL Persistent
critical Every turn + every tool call 30 min Yes
alert Every turn 1 hour Yes
info Once per session 4 hours No

agentihooks broadcast emit

AI-assisted broadcast composition. Describe the message in natural language and Haiku selects the appropriate severity, TTL, and wording.

agentihooks broadcast emit NATURAL_LANGUAGE_DESCRIPTION

The subcommand sends the description to Claude Haiku, which returns a structured broadcast (severity, TTL, message text) and immediately posts it.

# Haiku picks severity=critical, TTL=30m
agentihooks broadcast emit "prod API is returning 500s, stop all deploys immediately"

# Haiku picks severity=alert, TTL=8h
agentihooks broadcast emit "deploy freeze tonight until the on-call engineer clears it"

# Haiku picks severity=info, TTL=4h
agentihooks broadcast emit "sonarqube is down for maintenance"

Examples

# Emergency (manual)
agentihooks broadcast -s critical "Production incident — do NOT deploy"

# Deploy freeze (manual)
agentihooks broadcast -s alert -t 8h "Deploy freeze until 6am"

# Info (manual)
agentihooks broadcast -s info "SonarQube is down"

# AI-assisted emit
agentihooks broadcast emit "prod database is read-only until the migration completes"

# List / clear
agentihooks broadcast --list
agentihooks broadcast --clear

agentihooks channel

Channel-scoped broadcast publishing and inspection. A session only receives a channel-tagged message if its subscription list (from AGENTIHOOKS_BASE_CHANNELS) includes that channel. Global broadcasts (no channel) reach everyone.

Subcommands

agentihooks channel publish CHANNEL MESSAGE [-s SEVERITY] [-t TTL]
agentihooks channel list
Subcommand Purpose
publish Publish a message to a named channel. Same severity tiers as broadcast (info / alert / critical).
list Show active channels in ~/.agentihooks/broadcast.json with message counts per channel.

Subscriptions live in settings.json, not in CLI

There is intentionally no channel subscribe or channel unsubscribe subcommand. Subscriptions are operator-configured via the AGENTIHOOKS_BASE_CHANNELS env var, set in the profile’s settings.overrides.json env block (or per-repo via .claude/settings.local.json, or per-container via launch ENV). See Broadcast System → Channel Subscriptions.

Examples

# Tagged broadcast — only sessions subscribed to "deploy" see it
agentihooks channel publish deploy "Image roll-out paused on cluster-west" -s alert -t 1h

# Knowledge / brain content (the brain adapter publishes here automatically)
agentihooks channel publish brain "Hot arcs updated: 3 active" -s info -t 4h

# Inspect what's flowing
agentihooks channel list

agentihooks enforcement

Manage recurring reminders injected every N tool calls. Global runtime entries live in ~/.agentihooks/enforcements.json; --local scopes the operation to <git-root>/.agentihooks/enforcements.json.

agentihooks enforcement set "run tests before committing" 5
agentihooks enforcement set --local "read the operator directory" 10
agentihooks enforcement set "cluster writes go through GitOps" 1 --matcher bash.kubectl
agentihooks enforcement set --type rule --path rules/deploy.md
agentihooks enforcement list [--local]
agentihooks enforcement clear [--local] [--id <id> | --tag <tag>]

--local requires a Git project. Local files use the same JSON schema and cadence behavior as the global store. AgentiHooks creates the resource directory on the first local set, never edits Git ignore configuration, and leaves the directory in place after clear.

During injection, project-local entries are added to bundle, profile, and runtime entries. A matching local ID has highest precedence. The MCP tools take the same scope as local=true, resolved from the session’s project directory (or an explicit cwd).

--matcher limits an entry to matching tool calls (bash, bash.git, edit+write, mcp, mcp__<server>); its cadence then counts matching calls only. Grammar: Conditions.

--type rule --path <file> stores the canonical absolute path and reads the complete current UTF-8 file on every injection. Relative CLI paths resolve from the current directory. The MCP enforcement_set surface accepts the same type and path fields but requires an absolute path.


agentihooks ledger task split

Declare a code or CI task’s pull request halves before recording its first pull request:

agentihooks ledger --slug <id> --as <agent> \
  task split <task> 1-10 11-20

Ranges are ordered, non-overlapping plan lines and must cover the task’s assigned plan lines when present. A task with a recorded pull request, or a split with a recorded half, refuses the change. A task with no plan_lines and no plan_slice can name its halves by distinct labels instead, kept in the order given:

agentihooks ledger --slug <id> --as <agent> \
  task split <task> compatibility repair

Labels and ranges never mix, and a planned task still needs ranges. See Split tasks across pull requests for recording each half, the automatic active-link reset between merges, completion and troubleshooting.


agentihooks conditions

Inspect the conditions the running harness would execute.

agentihooks conditions list                                   # layers, conditions in order, invalid files
agentihooks conditions list --step post
agentihooks conditions list --tool Bash --command "cd x && git push"

The profile chain is read for AGENTIHOOKS_TARGET (default claude).


agentihooks recall

Read and build the recall archive of ledgers and swarms. The read commands print the same JSON as the recall MCP tools and exit 1 when the answer is "success": false.

agentihooks recall search "merge queue" --scope all --kind task --since 2d   # ranked hits
agentihooks recall get <ledger>/tasks/<id>                                    # one item, parents and comments
agentihooks recall timeline --kind task --kind comment --since 1d             # items oldest first
agentihooks recall sources                                                    # indexed ledgers and swarms
agentihooks recall reindex --all                                              # backfill from the stored ledgers
agentihooks recall eval golden.json                                           # top five hit rate

--scope defaults to your own swarm (every source outside a swarm); a ledger or swarm slug narrows it and all crosses swarms. --since and --until take an ISO time, epoch milliseconds or a span back from now such as 1d. search and timeline also take --author, --seat and --limit. --kind repeats.


agentihooks knowledge

Ask one ledger what it is and how it is evolving. The ledger is chunked by structure (ledger, phase, task, question, follow up, note, proof, comment), each chunk keeps its parent, and only new or changed chunks are embedded into a per ledger index under the agentihooks home. The default embedder counts hashed words, so matching is lexical. An ask refreshes the index first when its last refresh is two minutes old or more, so an added task is searchable within minutes.

agentihooks knowledge ask "merge queue priority" --limit 5   # summary, recent changes, hits with their parent chain
agentihooks knowledge refresh                                # embed what changed now

--ledger defaults to AGENTIHOOKS_SWARM. Each hit carries its parents, nearest first, so a task hit names its phase and a comment hit names its task and phase.

AGENTIHOOKS_KNOWLEDGE_EMBEDDER chooses the embedder: hash (default) or semantic, which matches by meaning, so a question worded differently from a task still finds it. semantic calls an OpenAI-compatible embeddings endpoint named by AGENTIHOOKS_KNOWLEDGE_EMBED_URL (HTTPS, or HTTP on loopback), AGENTIHOOKS_KNOWLEDGE_EMBED_MODEL, AGENTIHOOKS_KNOWLEDGE_EMBED_DIMENSION and AGENTIHOOKS_KNOWLEDGE_EMBED_TOKEN, sending at most 32 texts per request and ranking by cosine. A provider outage exits 1 with the reason. Each embedder and model keeps its own index file, so switching rebuilds rather than mixing vectors.


agentihooks claude / agenti

Launch Claude Code on one of the AH_CC_TOKEN_<slug> accounts, with --dangerously-skip-permissions and every other flag passed through. See Claude Account Load Balancing.

agenti                          # fewest live sessions among accounts below their band cap
agenti --route work             # force AH_CC_TOKEN_work, ignore the cap
agenti --model fable            # Fable models also weigh the separate Fable quota
agenti --route api              # force the api side; fails when no api endpoint is configured

The launch line reports account, routing_left and sessions=n/cap, or account=api kind=api for the api side. The api weight decides which side a launch takes; with every account at its cap the launch falls back to the api. When the api is full or absent too, the launch fails with no Claude account has a free session under its quota band.

agentihooks balance

Probe and rank every Claude account, plus the api row when an endpoint is configured. Columns: #, ACCOUNT, KIND (subscription, interactive, api; the Codex default login reads interactive, as routing offers it), STATE, SESSIONS (live/cap), WEIGHT (api rows), CAP, ROUTING LEFT and the five hour and weekly windows (n/a on an api row).

agentihooks balance                                  # probe (60 s cache) and rank
agentihooks balance --current                        # mark the account this session runs on
agentihooks balance --refresh                        # ignore the cache
agentihooks balance settings                         # every routing setting and the store in use
agentihooks balance set claude-api-weight=25         # live share of the api side, 0 to 100
agentihooks balance set codex-api-max-sessions=4     # api session cap; none clears it

balance set KEY=VALUE ... takes the keys claude-api-weight, codex-api-weight, claude-api-max-sessions, codex-api-max-sessions, master-account-claude, master-account-codex, master-tier-claude and master-tier-codex. It validates every pair before writing, prints store=redis or store=file <path> and one key: before -> after line per key, and exits 2 on an unknown key or an invalid value. See Claude Account Load Balancing.


agentihooks init-agent

Open a routed Claude session in a new terminal (WSL, macOS, native Linux).

agentihooks init-agent --dir ~/dev/repo --name fix-x --prompt-file task.md -- --model opus
agentihooks init-agent --handoff --dir "$PWD" --name repo-handoff --prompt-file handoff.md
Flag Meaning
--dir, --name, --prompt / --prompt-file Directory, session/tab name, opening prompt
--dry-run Print the launch without opening a terminal
--codex-transport legacy\|app_server, --seat Select the transport and admitted seat for a managed Codex swarm launch; see Managed Codex terminals for configuration and viewer behavior
--start-timeout Seconds to wait for the terminal to start (default 30)
--route-timeout Seconds to wait for the new session’s route report (default 150)
--canary-timeout Seconds to wait for the new session to run its profile canary (default the rest of the launch deadline); see Canary deadline
--launch-until Epoch milliseconds by which the launch must return; bounds the launch deadline. A hive passes its launch order’s expiry less an acknowledgement margin
--handoff Quota handoff: any account but this one, no bare-Claude fallback, marks this session handed off once the new one is routed; exit 3 on failure
--profile <name> Render this role profile once, record its home in the launch report and mount that home in the launched session; see Profile launch home
--overlay <name> Overlay the profile wears; repeat for up to three
--bundle-revision <commit> Render the profile only from this bundle commit. With a shared bundle linked the value has three parts, <root commit>:<shared commit>:<user commit> (<root commit>:<shared commit>: when no user bundle is linked); with only a user bundle linked it is <root commit>:<user commit>. The swarm records it at launch, with - as the shared field when the shared bundle has no commit to record (<root commit>:-:<user commit>). A value that omits the user bundle’s commit, or has an empty shared field, is refused rather than rendering the mutable tree. A - field, and a host without the recorded shared bundle, warn and render without it; a two-part value recorded before the shared bundle was linked renders with the shared bundle’s current commit, warns, and stamps that commit

Output is key=value lines: status=started, route_status (routed, bare, failed, pending), account, placement, and handoff=done|failed with --handoff.

Folder trust: before a Claude launch, a folder Claude has not trusted (neither it nor a parent has hasTrustDialogAccepted in $CLAUDE_CONFIG_DIR/.claude.json, else ~/.claude.json) is recorded as trusted, so the session never waits at the trust question. The launcher pins the session to that same config home: it exports the caller’s CLAUDE_CONFIG_DIR, or unsets it when the caller has none, so a herdr pane’s own environment cannot point Claude at another file. trust=trusted|marked|untrusted names the outcome; AGENTIHOOKS_TRUST_LAUNCH_DIR=0 turns marking off. On untrusted the launch still goes ahead and stderr says the session waits for someone to answer.

Hosts: herdr when installed and not disabled (state.json herdr.enabled), else the native terminal. --host herdr|native or AGENTIHOOKS_TERMINAL_HOST overrides. In herdr the session opens as a tab in the caller’s workspace, in --workspace <label> (crew), or in the repository’s workspace; --placement split|workspace changes that. The output adds workspace_id, tab_id, pane_id and agent_name.

Agents: --agent claude|codex; without it, the harness of the account the session rotation picks: the eligible Claude or Codex account with the fewest live sessions under its band cap (see Load balancing). Codex runs directly (route_status=direct).

With --profile, the output adds profile_validation=validated and profile_binding=<json> once the launched session has run its binding canary. The launcher starts the session through agentihooks select-profile NAME --home=<recorded home>, so a pull of dev or another code root between prepare and launch does not change the home the session mounts.


agentihooks select-profile

Select a profile, model and effort for one run, mount its rendered home and start agentihooks claude or agentihooks codex.

agentihooks select-profile engineer --agent claude -- --model opus
agentihooks select-profile engineer --home="$HOME/.agentihooks/profiles/.homes/engineer/<key>/claude" --agent claude --dry-run
Flag Meaning
name Profile to select
--agent claude\|codex\|copilot Harness (default claude); per-run profiles support claude and codex
--model, --effort minimal\|low\|medium\|high\|xhigh\|max Model and effort; otherwise the profile’s profile.yml defaults, then the harness defaults
--overlay <name> Wear this overlay; repeat for up to three
--bundle-revision <commit> Render only from this bundle commit; <root commit>:<shared commit>:<user commit> when a shared bundle is linked (- for a shared bundle with no commit), <root commit>:<user commit> when only a user bundle is
--home <dir> Mount this rendered home instead of rendering; refused unless its binding matches the profile and harness and its persona and sources are unchanged
--dry-run Print the selected environment and argv= line without starting the harness

Arguments after -- pass through to the harness. A refusal exits 2 with agentihooks select-profile: <reason> and fails a pending launch report. Home checks, refusals and troubleshooting: Profile launch home.


agentihooks headless run

Run one full rendered profile unattended on Claude Code or codex-cli and print one JSON object with status, output, session, exit_code, transcript and run_dir. Exits 0 only when status is ok.

agentihooks headless run engineer --input 'Reply with JSON whose answer is the word pong.'
agentihooks headless run engineer --input prompt.md --schema schema.json --harness codex --tier low --timeout 600 --out runs/codex
Flag Meaning
PROFILE Profile to run
--input TEXT\|FILE Prompt text, or a file holding it (required)
--schema FILE JSON schema file the output must match
--harness auto\|claude\|codex Harness (default auto: AGENTIHOOKS_TARGET, else claude)
--out DIR Run folder (default <AGENTIHOOKS_HOME>/headless/<UTC stamp>-<8 hex>)
--tier high\|low, --effort Model tier (default high) and effort (default medium)
--timeout SECONDS Kill the run after this many seconds (default 1800)

Statuses, run folder, tiers, worker mode and troubleshooting: Headless profile runs.


agentihooks terminate-agent

List live Claude Code and Codex sessions, end one by exact name, session UUID or PID, or stand down one managed Codex conversation without ending its process.

agentihooks terminate-agent --list --type any        # TYPE NAME SESSION_ID PID PGID STATUS CWD
agentihooks terminate-agent eng@abc-0042 --dry-run   # resolve and validate, send nothing
agentihooks terminate-agent eng@abc-0042             # SIGTERM the process group, SIGKILL if it survives
agentihooks terminate-agent ci@abc-0815 --stand-down --dry-run
agentihooks terminate-agent ci@abc-0815 --stand-down --swarm <slug>
Flag Meaning
--type claude\|codex\|any Limit listing and matching to one harness (default any)
--list Print the live sessions table
--dry-run Resolve and validate, print what would happen, change nothing; ends result=validated signal=none
--force-shared Allow ending a process whose other session UUIDs would end with it
--stand-down Stand down one managed Codex conversation instead of signalling a process
--swarm <slug> Swarm whose Codex records the stand-down reads; without it, the swarm the agent name belongs to
--term-timeout Seconds to wait after SIGTERM, and again after SIGKILL (default 3)
--keep-pane Leave the agent’s herdr pane open after it exits
--json Print the resolution report as one JSON object

Terminate a process

A termination validates the target first: the PID’s start time, command and arguments match its listing, its process group ID is above 1, every member belongs to one POSIX session, and the group does not contain the caller. It then sends SIGTERM to the whole process group, waits --term-timeout, rechecks the target’s identity and sends SIGKILL to any survivor. Output is the resolution line (action, type, name, session_id, pid, pgid, members, pane_id in herdr), then result=terminated escalation=SIGKILL|none and, in herdr, pane_id=<id> pane=closed|kept|close-failed (...). Exit 2 is a refused resolution or validation; exit 1 is a failed signal or a process that outlived SIGKILL.

One interactive Codex process can carry several conversations, each listed as its own row with the same PID. Ending that process ends all of them, so a termination refuses it with target process is shared by other session UUIDs: <ids>; use --force-shared. Add --force-shared only when every listed conversation belongs to the agent being stopped.

Stand down one Codex conversation

--stand-down stops one conversation of a managed Codex agent, one the swarm launched on the Codex app-server and recorded in its Codex transport records, and sends no signal. The selector is matched against each record’s agent name, thread id, session id and execution id in the swarm. A stand-down:

  1. Marks that execution’s transport record stood_down. The agent’s inbox pump treats the execution as no longer current and stops, so the conversation takes no new turns from the inbox.
  2. Waits up to 10 seconds for an inbox publication to that seat already in flight to finish.
  3. Reads the thread from the app-server and, when its last turn is in progress, interrupts that turn with turn/interrupt, then waits up to 10 seconds for the turn to stop.

The app-server process and every other conversation it hosts keep running. Under --stand-down only --swarm and --dry-run change the run; --type, --json, --force-shared and --keep-pane are ignored, --term-timeout is only checked to be positive, and --list prints the listing instead.

$ agentihooks terminate-agent ci@abc-0815 --stand-down --dry-run
action=stand-down-dry-run
stops: conversation <thread> of ci@abc-0815 (execution <id> generation 3): its active turn (last recorded: <turn>) is interrupted and it takes no new turns
keeps: app-server pid 41872: not signalled
keeps: conversation <uuid> of eng@abc-0816: not interrupted
result=validated signal=none

The dry run reads the swarm’s recorded transport state and the live process list, and does not contact the app-server, so last recorded: is the turn the inbox last submitted or reconciled, or none. Without --dry-run the same stops: and keeps: lines print, the stand-down runs, and the command ends result=stood-down stood_down=<agent> thread=<thread> interrupted=<turn>; the run interrupts only a last turn the app-server reports in progress, and interrupted= is empty when none was.

Exit Output Meaning and next step
0 result=validated signal=none Dry run; nothing changed
0 result=stood-down ... Fenced and, when a turn ran, interrupted
1 terminate-agent: <error> then <agent> stays fenced from new inbox turns; run the same stand down again to interrupt its turn The fence is written first, so the record is stood_down after this exit. Causes: an inbox publication still in flight, an unreachable app-server, or a turn still running after the interrupt. Rerun the same command. A Redis error or an unreadable transport record raises an uncaught traceback, also exit 1, with no stays fenced line; check the record’s state before rerunning
2 terminate-agent: refused: <reason> then terminate-agent: safe alternative: <alternative> Nothing changed; see the refusals below
Refusal reason Safe alternative
No --swarm and the selector names no swarm agent pass --swarm <slug>
The selector has no managed Codex conversation in the swarm, and it names one process That process’s PID and conversation ids, with agentihooks terminate-agent <pid> --force-shared --dry-run; the same command without --dry-run ends every conversation in the process, so run it only when all of them belong to this agent. An empty list means the process hosts no registered conversation: skip --force-shared and use the plain agentihooks terminate-agent <pid> --dry-run
The selector has no managed Codex conversation, and it names no single process agentihooks terminate-agent --list --type codex shows the live conversations
The conversation already ended (exited or failed) nothing needs standing down
The selector names several live conversations name one thread: <thread ids>

agentihooks herdr

Install and configure herdr, the terminal host init-agent opens agents in. The first agentihooks init on a terminal asks once whether to use herdr and records the answer in state.json (herdr.enabled); agents pass agentihooks init --herdr yes|no. Later inits with herdr enabled install the binary when missing and (re)install the herdr integrations for Claude and Codex.

Configure also writes [session] resume_agents_on_restore = false into the herdr config, overwriting true. A herdr server restart then brings agent panes back as plain shells; agents return only through the agentihooks boot recovery and launcher, which give each one its routed account. herdr’s own resume runs codex resume <id> or claude --resume <id> without that account.

agentihooks herdr              # status; offers to install when herdr is missing
agentihooks herdr install      # official installer, then configure
agentihooks herdr configure    # herdr integration install claude|codex; agent resume off
agentihooks herdr disable      # init-agent stops choosing herdr

agentihooks quota

Quota left for every agent harness: one row per Claude account (router cache, or a live probe with --refresh) and one for Codex, read from the newest rate-limit event in Codex’s session logs.

agentihooks quota            # AGENT ACCOUNT KIND STATE SESSIONS WEIGHT CAP 5H LEFT 5H RESET 7D LEFT 7D RESET SOURCE
agentihooks quota --json

agentihooks classify, classifier stats, classifier eval

Ask the LiteLLM decision models typed questions, and read the decision log. Details in Decision classifier.

agentihooks classify --state state.json --questions questions.json [--purpose P]
agentihooks classifier stats [--purpose P]
agentihooks classifier eval NAME [--live N]

agentihooks balance

agentihooks balance              # every account: state, SESSIONS n/cap, routing left, resets
agentihooks balance --current    # which account this session runs on
agentihooks balance --refresh    # ignore the 60-second quota cache
agentihooks balance --fable      # include the Fable weekly quota

SESSIONS counts live interactive Claude processes per account from /proc; sessions on no account are listed under the table as unrouted.


agentihooks gc, lease, scratch

Workspace lifecycle for agent-created directories: git worktrees, scratch dirs and tool files.

agentihooks gc                     # report what is safe to remove
agentihooks gc --enforce           # act on findings that are due: remove, snapshot to wip/, archive
agentihooks gc --json              # full report; also written to ~/.agentihooks/gc-last.json
agentihooks gc --path ~/scratchpad/myrepo
agentihooks gc --install-timer     # hourly agentihooks scheduler cron (agentihooks install does this when LIFECYCLE_GC_ENABLED)
agentihooks gc --remove-timer
agentihooks lease <worktree>       # record the calling agent session as owner (wt.sh new calls this)
agentihooks lease <dir> --kind scratch|ephemeral
agentihooks scratch new                 # mkdir ~/scratchpad/<repo>/<task> + lease, prints the path; the name is built
                                        # from the session (<swarm>-<task> or its session base), another name is refused
agentihooks name worktree|tmp [--check NAME]  # the built worktree name wt.sh uses: the session base, then -2, -3
agentihooks host-guard install|check     # link and schedule the host process guard, or report its drift;
                                        # see host-process-guard.md
agentihooks scratch rm <task-dir>       # safe replacement for rm -rf "$dir": refuses when another
                                        # process works inside, another live session holds the lease,
                                        # or a nested worktree has uncommitted or unpushed work

A path is actionable only when it is unused and removing it loses nothing:

  • Unused: no live process has its cwd inside it, no lease holder is alive (pid + /proc start time + boot_id, or a live session file with the same sessionId and matching procStart), nothing in it changed recently (except a clean pushed worktree whose lease holders are all gone), and the machine has been up at least 2 h. A worktree untouched WT_STALE_DAYS days skips the process, git lock, git operation and uptime checks; only the primary checkout, a protected branch and a live lease holder keep it.
  • Worktrees: clean and reachable from a remote after 2 h idle (remove), or at once when its lease names holders and none is alive; dirty or unpushed after 24 h idle (snapshot — pushed to wip/ first; in a scratch repository the snapshot is also kept as a local wip/ branch and a failed push still removes the worktree); any worktree untouched WT_STALE_DAYS days (7, a non-numeric or non-positive value counts as 7), whatever holds it except a protected branch or a live owner. Primary checkouts and dev/main/master are never touched; git-locked worktrees and a merge/rebase in progress are kept until they go stale.
  • Scratch task dirs (~/scratchpad/<repo>/<task>): idle past the root’s idle_days, then the oldest dead dirs while the root is over budget_gb. A .keep file pins a dir; files at depth ≤ 2 are never touched.
  • Injection traces (~/.agentihooks/injections/<session>.jsonl): removed once the session is no longer live and the trace was last written more than AGENTIHOOKS_TRACE_KEEP_DAYS ago (default: the root’s idle_days, 14). Liveness is checked again right before removal.
  • An action is due only after two sweeps at least 1 h apart in the same boot, except removal of a clean pushed worktree whose owner left, which is due on first sight. --enforce re-checks each due path against a fresh process snapshot right before acting.
  • snapshot builds a commit from the worktree through a temporary index (ignored files and files over 50 MB are left out and listed), with [skip ci] in the message, and pushes it to wip/<repo>/<worktree>-<timestamp>. A failed push keeps the worktree, except in a scratch repository, where the snapshot stays on a local wip/ branch and the worktree is removed.
  • Removals are journaled in ~/.agentihooks/gc-journal.json; the next sweep finishes any removal a crash interrupted.
  • Herdr panes: init-agent (every swarm, resume and proof launch) and run-in-terminal with a command record each pane they open in ~/.agentihooks/herdr/panes/ (pane id, terminal id, tab, workspace, owner swarm and session, launch time, route status). gc lists, and gc --enforce and every swarm tick close, a recorded pane whose agent is gone: it sits at a bare shell (the agent or command exited, or the launch failed), its swarm agent was retired, or its swarm stopped or was removed. Never inside AGENTIHOOKS_HERDR_GC_GRACE_MINUTES (default 5) of launch, while its input line holds text, or while its screen changed or its agent was prompted inside AGENTIHOOKS_INBOX_QUIET_S; a pane whose terminal id changed, or a swarm store that cannot be read, keeps the pane. A pane agentihooks never recorded, such as a plain run-in-terminal shell, is never touched. A tab or workspace the sweep empties is closed. Each close prints with its reason, herdr: closed pane … in the tick journal (journalctl --user -t agentihooks-swarm).
  • Codex execution homes ($XDG_RUNTIME_DIR/cx and ~/.agentihooks/cx): a home no live process owns, by working folder, argument, open file or an answering server socket, and unchanged for ten minutes is listed under codex_homes in the JSON report and removed by --enforce on first sight, after its rollouts are appended to the canonical Codex home. Removals are logged to ~/.agentihooks/gc-codex-homes.jsonl. See Reclaim orphan execution homes.
  • Launch run folder ($XDG_RUNTIME_DIR/agentihooks-claude-terminal): launch scripts, route reports, prompts, start markers and profile reports older than a day are removed, and closing markers whose launcher has exited.

Triggers: an agentihooks-gc cron on the agentihooks scheduler (0 * * * *) starts gc --enforce at the top of every hour; install and upgrade remove the older agentihooks-gc.timer systemd unit. Every PreToolUse call that names a path inside a managed worktree or scratch task dir records the session as a lease holder; while gc is removing that path the call is blocked with a retry message. When free space (the smaller of $HOME and, on WSL, /mnt/c) drops below AGENTIHOOKS_DISK_WARN_GB, tool calls get a warning (at most every 10 min) and a sweep starts at once. SessionStart and SessionEnd start a sweep at most every AGENTIHOOKS_GC_INTERVAL_MIN, as a detached hook task.

Roots come from profiles/_base/lifecycle.json, overlaid by lifecycle.json in the bundle, each active profile and ~/.agentihooks/lifecycle.json, merged by id ("enabled": false drops a root). Kinds: worktrees, scratch, ttl (delete idle entries), archive (gzip idle files matching include), traces (delete injection traces of ended sessions).

Git maintenance and object counts

Lifecycle setup registers the primary checkout behind each discovered managed linked worktree for native Git maintenance. Registration uses the global Git maintenance.repo entries, so several worktrees of one repository produce one primary checkout entry. A repository with no discovered managed linked worktree is outside this discovery path.

agentihooks gc --install-timer

This installs the hourly lifecycle sweep and checks maintenance registration on every setup run. agentihooks init also runs this setup when LIFECYCLE_GC_ENABLED=true (the default). Missing registrations use git maintenance start in the operating system user’s real home, which asks Git to schedule its native maintenance. With a redirected HOME, setup uses git maintenance register without starting the user’s scheduler. Already registered repositories are left registered without another start attempt; registration alone does not prove the native Git scheduler is healthy.

A successful new registration prints, for example:

[OK] git maintenance registered for /home/me/dev/myrepo

Subsequent setup runs print no maintenance line for that repository while its registration remains present. Every agentihooks gc --enforce sweep also repairs missing registrations immediately, without waiting for the cleanup confirmation period. It can perform the other due cleanup actions described above too. agentihooks gc without --enforce only reports registration and object counts.

Configure discovery

The existing lifecycle roots determine which linked worktrees are discovered. For example, this personal ~/.agentihooks/lifecycle.json adds a second worktree root alongside the default ~/dev/worktrees:

{
  "roots": [
    {"id": "extra-worktrees", "path": "~/projects/worktrees", "kind": "worktrees"}
  ]
}

Place linked worktrees under <root>/<repo>/<worktree> (or <root>/<repo>/_tmp/<worktree>). Nested linked worktrees in configured scratch roots are also discovered. See lifecycle.json for overlay precedence and root settings. There is no separate maintenance root list or backlog threshold setting.

Read a checkout’s backlog

agentihooks gc
agentihooks gc --json | jq '.git_maintenance'
git config --global --get-all maintenance.repo
git -C ~/dev/myrepo count-objects -v

The git lines show the number of loose objects (count from git count-objects -v) and pack files (packs), rather than sizes or the number of packed objects. Example report-only output with a missing registration:

git       78532 loose    47 packs  /home/me/dev/myrepo  (maintenance not registered)

The corresponding entry in the JSON report and ~/.agentihooks/gc-last.json is:

{
  "path": "/home/me/dev/myrepo",
  "registered": false,
  "outcome": "",
  "loose": 78532,
  "packs": 47
}

registered reflects membership in maintenance.repo, including a successful registration during this sweep. outcome is empty when no registration was attempted, registered when the attempt succeeded, or failed: <reason> when it failed. Text output uses that outcome, or maintenance registered / maintenance not registered when it is empty.

Counts are sampled on every completed sweep that reaches maintenance reporting; registration does not run compaction synchronously or guarantee an immediate reduction. Compare counts over time to spot a growing backlog before Git warns. The sweep reports counts without raising a threshold alert. A scoped sweep with --path includes a maintenance entry only if the primary checkout lies within that scope. Scoping to its separate linked worktree directory can omit it; use an unscoped sweep to see all discovered primaries.

Troubleshooting

A registration failure prints [!!] git maintenance failed: <reason> for <path> during setup, or failed: <reason> in the sweep entry. Check Git’s reported scheduler or permission error, correct that cause, then repeat setup or an enforcing sweep. Git execution errors and timeouts are reported per checkout so other checkout entries can still be collected.

If object counting fails, JSON counts are null and text counts are None; these mean unavailable, not zero. Run git -C <primary> count-objects -v to see Git’s diagnostic. If an entry is absent, check configured roots, the presence of a managed linked worktree and any --path filter. If a registered checkout keeps accumulating objects, inspect the native Git maintenance scheduler separately: setup repairs a missing registration, but does not restart an already registered repository’s schedule or clear previous Git garbage collection errors.

Worktree verdicts

Each worktree gets the first verdict that applies, checked top to bottom. The reason is the text gc prints in parentheses after the path.

A worktree untouched for WT_STALE_DAYS days (default 7) loses every keep and skip below except the primary checkout, a protected branch and a live lease holder: it is removed, pushed to wip/ first when it holds uncommitted or unpushed work, with the reason untouched over <days> days though <the hold it overrode>.

Condition Action Reason Due
The repository’s primary checkout keep primary checkout never
On dev, main or master keep protected branch <name> never
Locked by git worktree lock and no lease keep git-locked never
A merge, rebase, cherry-pick, revert, bisect or index lock in the worktree’s git dir skip git <marker> in progress, the marker file such as MERGE_HEAD never
A live process has its cwd inside keep a process works inside never
A lease holder is alive keep owner session alive never
The machine has been up under 2 h keep boot grace never
Throwaway lease (--kind ephemeral, wt.sh tmp), idle 2 h, even with uncommitted or unpushed work remove throwaway checkout, owner gone after confirmation
Throwaway lease, idle under 2 h keep throwaway, recent never
Clean, head on a remote, lease names holders and none is alive remove clean and on a remote, owner left on first sight
Clean, head on a remote, no lease or a lease with no holders, idle 2 h remove clean and on a remote after confirmation
Clean, head on a remote, no holders, idle under 2 h keep clean, recent never
Uncommitted changes or a head on no remote, idle 24 h snapshot unique work, owner gone: push to wip/ then remove after confirmation
Uncommitted changes or a head on no remote, idle under 24 h keep unique work, recent never

A snapshot of a worktree whose primary checkout sits inside the same scratch root adds ; scratch repository: removed if the push fails to its reason: the snapshot is first kept as a local wip/ branch in that primary checkout, and when the push fails the worktree is still removed, so a test fixture with an unreachable origin cannot hold its scratch folder forever. The local branch stays after a successful push too, and it lives only as long as the scratch folder that holds the repository.

Clean means git status --porcelain lists nothing, untracked files included. Head on a remote means git rev-list HEAD --not --remotes is empty, so a branch pushed under any name counts. Idle time runs from the newest of the worktree’s index, HEAD, reflog and changed files. After confirmation means the action is due once two sweeps in the same boot, at least 1 h apart, gave it the same action; any gc run, report-only included, counts as a sweep. In the report, the second column is due when --enforce acts on the line and pending while it waits for that confirmation.

The owner left verdict ignores idle time and confirmation. wt.sh new and agentihooks lease <worktree> record the calling agent session as a holder when one is found among the parent processes, and every tool call that names a path inside the worktree adds its session. Once each of those sessions has ended, a clean pushed worktree goes at the next gc --enforce, usually within the hour. The process, live holder and boot grace checks above still come first, and --enforce repeats them against a fresh process snapshot before it removes anything. A worktree nobody leased becomes due once it has been idle 2 h and two sweeps at least 1 h apart have seen it so, and the verdict never applies to unpushed or uncommitted work.

When the repository is at the WT_MAX_PER_REPO cap (60) or free space is below WT_MIN_FREE_GB (50), wt.sh new and wt.sh tmp first run agentihooks gc --make-room <repo> --root <worktree root> --min-free-gb <floor> --max-per-repo <cap>. It evicts the oldest worktrees under the worktree root not held by the primary checkout, a protected branch, a git operation, a process inside or a live lease holder, and untouched for at least a minute so a worktree another wt.sh new just created is never taken: clean pushed ones are removed, unique work is pushed to wip/ and then removed, and one whose push fails is kept. The cap evicts only that repository’s worktrees; the floor evicts from every repository under the root. The cap counts every worktree git lists for the repository, so one created outside the root by hand still counts and is never evicted. wt.sh refuses only when nothing safe is left, and prints worktree eviction failed (exit N) first when the eviction run itself fails. To free a repository by hand:

agentihooks gc --path ~/dev/worktrees/agentihooks          # report; owner left removals print as due
agentihooks gc --enforce --path ~/dev/worktrees/agentihooks
remove    due         0.02 GB  /home/me/dev/worktrees/agentihooks/engineer-0007  (clean and on a remote, owner left)
remove    pending     0.02 GB  /home/me/dev/worktrees/agentihooks/notes  (clean and on a remote)

engineer-0007 is removed by this sweep. notes has no lease holder, so it waits for a second sweep an hour later. A worktree still listed as keep with owner session alive belongs to a running agent: finish that agent’s work and run wt.sh done <name> from it.

Lease holders and resumed agents

A worktree lease is agentihooks-lease.json in the worktree’s git admin directory (git rev-parse --absolute-git-dir from inside the worktree); a scratch task dir keeps .lease.json at its top. The lease lists up to 8 holders, each the agent process that claimed it:

Field Value
session_id The hook payload’s session id: the Claude session or the Codex conversation
pid The nearest parent of the hook process that is claude or codex, searched at most 32 levels up
start_time That process’s start time in clock ticks, field 22 of its /proc stat file
boot_id The kernel boot id when the holder was recorded

gc counts a holder alive when its pid runs with the same start_time in the same boot, or when a Claude session file in ~/.claude/sessions names the same sessionId with a live pid and matching procStart. Codex writes no such file, so a Codex holder is alive only while its recorded process runs.

On every PreToolUse call the lifecycle guard collects candidate paths from the tool input: file_path, notebook_path, path, workdir, and every token in command that starts with / or ~. For each managed worktree or scratch task dir those paths fall in, it records the calling agent process as a holder unless that session already has a holder whose process still runs. Holders are keyed by session id, so a new process under the same session replaces the old entry instead of adding one.

This keeps a resumed agent’s worktree leased. When an agent exits and resumes the same conversation in a new process (a Codex pane restart, a recovered seat), the lease still names the old pid, which is gone. The resumed agent’s first tool call that touches the worktree replaces that holder with its live process. A Codex Bash call reaches the guard with its folder in tool_input.workdir, which normalize_payload copies from the rollout when the payload lacks it (see Codex compatibility), so a Codex agent re-claims its worktree even when its commands name no path.

Before pull request #4915 the claim skipped any session already listed in the lease, live or not, and did not read workdir. On 19 October a Codex engineer resumed after its pane exited; its lease still held only the process wt.sh new recorded, and the hourly sweep removed its clean, pushed worktree as clean and on a remote, owner left while its draft pull request waited on checks.

Read a worktree’s lease and check each holder against /proc:

cd ~/dev/worktrees/agentihooks/engineer-0007
python3 -m json.tool "$(git rev-parse --absolute-git-dir)/agentihooks-lease.json"
PID=628154                                   # a holder's pid from the lease
awk '{print $22}' "/proc/$PID/stat"          # must equal that holder's start_time
cat /proc/sys/kernel/random/boot_id          # must equal its boot_id
{
    "kind": "worktree",
    "created_at": 1792742140.8260634,
    "holders": [
        {
            "session_id": "b0d53d40-38e3-4e82-bca1-1cb83a0d85a0",
            "pid": 628154,
            "start_time": 5683458,
            "boot_id": "64f5efa7-8967-4733-916b-9fcd69669c00"
        }
    ]
}

When awk prints 5683458 and the boot ids match, the holder is alive: agentihooks gc --json --path <worktree> carries the worktree in findings with action keep and reason owner session alive, or a process works inside when a shell sits in it, since that check comes first (the text report prints no line for a keep). A missing stat file or another start time means that holder is dead; a worktree whose holders are all dead is removed at the next sweep once it is clean and pushed.

Troubleshooting:

  • A running agent’s worktree was removed as clean and on a remote, owner left. Its lease named no live process at the sweep. An agent that resumed and since only waited (a checks wait, an idle pane) made no tool call naming the worktree, so its old holder stayed dead. Any tool call with a path or workdir inside the worktree re-claims it; agentihooks lease <worktree> run from the agent’s shell records it directly.
  • The lease holds a dead pid for a live session. The agent has not run a tool call inside the worktree since it resumed. Its next such call replaces the entry.
  • A tool call records no holder. The hook found no claude or codex process among its parents, the path lies outside every worktrees and scratch root, or the worktree has no .git file pointing to an admin directory. A relative workdir never matches a root. lifecycle pre-tool failed in the hook log means the claim raised and the call went through unrecorded.
  • Finding what removed a worktree. ~/.agentihooks/gc-last.json holds every finding of the latest gc run with its outcome; the next run overwrites it. ~/.agentihooks/gc-journal.json lists only removals still in progress. A worktree gone with no finding in the run after it last existed was removed by something else, such as wt.sh done from another session.

lifecycle.json

Every layer is a JSON object with a roots list. Each root entry takes:

Field Default Meaning
id required Merge key across layers; an entry without a non-empty id is ignored
path required in the merged entry Directory the root covers; ~ is expanded
kind required in the merged entry One of the kinds above; any other value drops the root
idle_days 7 Idle age before an entry is acted on
budget_gb 0 scratch only: size above which the oldest dead dirs go
include [] ttl and archive: list of file name globs the root acts on; empty means every entry. Dot names and the archive folder are always skipped
enabled true false drops the root after the merge

Later layers update an earlier entry with the same id field by field, so an overlay names only what it changes. A personal ~/.agentihooks/lifecycle.json that shortens the scratchpad idle time, turns off ledger archiving and adds a root of its own:

{
  "roots": [
    {"id": "scratchpad", "idle_days": 3},
    {"id": "ledgers", "enabled": false},
    {"id": "build-cache", "path": "~/.cache/mybuild", "kind": "ttl", "idle_days": 2}
  ]
}

Each finding in agentihooks gc --json carries the id of the root it came from in its root field.

A layer the reader cannot open or parse contributes no roots and the other layers still load: a missing file, a path that is a directory or cannot be read, text that is not JSON, JSON whose top level is not an object (null, []) and an object with no roots all count as empty. Inside roots, entries that are not objects or have no id are skipped. Nothing reports a skipped layer or entry, so a broken overlay leaves the other layers’ roots in force without a warning.

Field types are not checked. These shapes raise out of the reader, which stops agentihooks gc with the error; on PreToolUse the lifecycle guard logs lifecycle pre-tool failed and lets the call through without recording a lease or blocking a path gc is removing:

  • A file that is not valid UTF-8.
  • roots set to null or a number; it must be a list.
  • path set to a number, or kind set to a list or object.
  • include set to null. include must be a list of strings: a bare string such as "*.json" is read as one pattern per character.

idle_days and budget_gb must be numbers: a string loads without error, and the sweep can fail later when it computes an entry’s age limit or the scratch size budget from it.


agentihooks refresh-rules

Re-render every installed profile home from the bundle checkout and push the new rules into every running Claude Code session without a restart. Each target session consumes the refresh once on its next UserPromptSubmit.

agentihooks refresh-rules [--profile <name>] [--dry-run] [--clear]

agentihooks init re-renders only the profile homes of its own install target. It refuses to run in a rendered profile home or with AGENTIHOOKS_SWARM or AGENTIHOOKS_PROFILE set. refresh-rules runs there and re-renders the homes of every harness, so a manifesto or rule merged into the bundle reaches every rendered profile and its running sessions with no operator step.

How it works

  1. Reads the installed rules: ~/.claude/CLAUDE.md and every ~/.claude/rules/*.md. In a rendered profile home (CLAUDE_CONFIG_DIR) it re-renders the profile first; that home has no rules/ folder, its CLAUDE.md carries persona and rules together. In ~/.claude it first copies the bundle rule files recorded at install into ~/.claude/rules.
  2. Takes a snapshot of currently-alive session IDs from the broadcast registry.
  3. Writes ~/.agentihooks/force_refresh/rules-<profile>.json containing the payload + pending session list.
  4. Re-renders every other installed profile home for each harness, claude, codex and copilot, from the bundle as the checkout holds it now, and prints [OK] Re-rendered the <key> profile home for each. It skips only the Claude home of the refreshed profile, the one --profile names or the one detected. Run without --profile from a rendered home, that is the home step 1 already rendered. Codex and Copilot homes get no marker; their sessions load the new rules at their next start.
  5. Writes a refresh marker for every re-rendered Claude home, rules-<key>.json with that home’s new rules, and prints [OK] Refresh marker written for profile '<key>' (<n> sessions).
  6. On each targeted session’s next UserPromptSubmit, the hook checks that the session runs from the profile home the marker names, injects the rules of that profile’s current home and removes the session from pending. A session of another profile is removed from pending with nothing injected.
  7. When pending drains → marker deleted. Otherwise marker auto-GCs after 24h.

Sessions started AFTER the push never see the marker — they get fresh rules at SessionStart, so re-injection would be redundant.

A rendered Claude home lives at ~/.agentihooks/profiles/.homes/<key>/<render>/claude, and ~/.agentihooks/profiles/.homes/<key>/current points at the newest render. Each re-render moves current; a session started earlier keeps running from its older render folder. When it consumes the marker, the hook reads the rules from current, so it receives the merged doctrine.

The render reads the bundle checkout linked with agentihooks init --bundle <path> as it stands, so pull the merged commit into that checkout first. In a Doctor swarm, agentihooks doctor <slug> intervene refresh-rules, where <slug> is the watched ledger or its Doctor ledger, runs this command and logs The Doctor refreshed the installed rules for every running session. on both ledgers.

Flags

Flag Description
--profile <name> Profile name (default: the profile of the rendered home it runs from, else detected from the ~/.claude/CLAUDE.md symlink target, else anton)
--dry-run Print what would be pushed (profile, content hash, payload size, target session IDs) without writing the marker or re-rendering any home
--clear Delete any existing pending marker for the profile (cancel a push in progress)

Examples

# Preview which sessions would be hit
agentihooks refresh-rules --dry-run

# Push the current rules to all alive sessions
agentihooks refresh-rules

# Cancel a pending marker without waiting for TTL
agentihooks refresh-rules --clear

# After a bundle merge: pull it into the linked bundle checkout, then refresh
git -C ~/dev/agentihooks-bundle pull --ff-only origin dev
agentihooks refresh-rules

# The same refresh as a Doctor intervention on the watched swarm
agentihooks doctor my-swarm intervene refresh-rules

Troubleshooting

  • A profile whose render is refused keeps its previous home, and the command prints [--] Profile home <key> kept as it was: <reason>. Fix the reason and run the command again. Any other render error stops the command; homes after that profile are not re-rendered and get no marker.
  • --profile naming a profile other than the home it runs from skips that profile’s Claude home without re-rendering it. Run the command without --profile, or from that profile’s home.
  • A session running from a legacy home, a real directory outside .homes, receives the refresh only when the command runs from that same home.

agentihooks mcp

Two unrelated jobs behind one word: a surface-area report, and the agentihooks daemon’s lifecycle.

agentihooks mcp report [--project PATH]   # tool/token surface across every configured server
agentihooks mcp status                    # daemon: configured vs. actually running
agentihooks mcp start
agentihooks mcp restart
agentihooks mcp stop

The daemon subcommands

These do nothing under stdio, which is the default — Claude Code spawns agentihooks per session and there is no daemon. They exist for the network transports; see MCP Transport.

agentihooks init starts the daemon itself, so start is only needed after a reboot on a machine with no systemd user session, where the fallback backend has no supervisor behind it.

status and its exit codes

status is the one worth knowing. It prints the configured transport and endpoint, what ~/.claude.json declares, which supervisor is in play, whether the process is up, whether the port answers, and names every mismatch between them.

Exit Meaning
0 running, and matching config
1 stopped
2 running, but diverged from config

The divergence case is why this exists. A daemon started before a config change keeps serving the old transport or port while ~/.claude.json names the new one, and nothing else reports it — the only symptom is tools that quietly fail to appear.

Supervisors

Backend Chosen when Survives reboot
systemd systemctl --user reaches a user bus yes
pidfile everything else — WSL2 without systemd=true, containers, macOS no

AGENTIHOOKS_MCP_SUPERVISOR=systemd\|pidfile forces one. An unrecognised value warns on stderr and falls back to detection.

The pidfile backend records ~/.agentihooks/mcp-daemon.pid and logs to ~/.agentihooks/logs/mcp-daemon.log, rolled to .log.1 past 5 MB.


agentihooks uninstall

Remove everything agentihooks installed from the system.

agentihooks uninstall [--yes]

What gets removed

  • ~/.claude/settings.json – if managed by agentihooks (detected via _managedBy marker)
  • Skills, agents, commands, and rules symlinks in ~/.claude/ – if they target the agentihooks repo
  • ~/.claude/CLAUDE.md – if it points into profiles/
  • MCP servers in ~/.claude.json – from profile .mcp.json files and state.json
  • The agentihooks daemon – stopped under both backends, and its systemd unit removed. Uninstall verifies afterwards and warns if a process survived, since an orphaned daemon with the CLI gone has nothing left to manage it
  • Bashrc block – the agentienv function and agenti alias are removed from ~/.bashrc
  • agentihooks CLI – via uv tool uninstall agentihooks

What is NOT removed

~/.agentihooks/ (user data: logs, memory, state.json) is left in place. To fully reset:

rm -rf ~/.agentihooks

Flags

Flag Description
--yes Skip confirmation prompt (for scripting)

agentihooks claude

Launch Claude Code with --dangerously-skip-permissions and pass through any extra args.

agentihooks claude [extra-args...]

Alias: agenti (installed by agentihooks init in the bashrc block)

How it works

The launcher injects exactly one flag: --dangerously-skip-permissions. Any extra arguments are appended verbatim. Model, effort, and other Claude Code defaults come from ~/.claude/settings.json (rendered from each profile’s settings.overrides.json).

The claude: block in profile.yml was removed 2026-05-07; profile-level CLI flag mapping no longer exists.

Examples

# Launch
agentihooks claude

# Use the alias
agenti

# Pass extra args to claude
agenti --model haiku --verbose

agentihooks ignore

Create a .claudeignore in the current working directory (or a given path). Claude Code uses .claudeignore to exclude files from reading and indexing – keeping credentials, build artefacts, and binaries out of the context window.

agentihooks ignore [path] [--force]

What it creates

A .claudeignore covering:

Section Examples
Credentials & secrets .env, .env.*, *.pem, *.key, secrets/
Build artefacts __pycache__/, dist/, node_modules/, target/, *.egg-info/
Runtime data *.log, *.sqlite, *.db, *.lock
Test output .coverage, htmlcov/, junit*.xml
IDE / OS noise .idea/, .vscode/, .DS_Store, Thumbs.db
Large binaries / media archives, images, video, fonts
Virtual environments .venv/, venv/, env/
IaC state .terraform/, *.tfstate, .terraform.lock.hcl

.env.example is explicitly un-ignored (!.env.example) so the template remains visible.

Flags

Flag Description
path Target directory (default: current directory)
--force Overwrite an existing .claudeignore

Examples

# Create in current directory
agentihooks ignore

# Create in a specific project
agentihooks ignore ~/dev/my-project

# Overwrite an existing file with a fresh template
agentihooks ignore --force

agentihooks --list-profiles

Print all available profiles and exit. Shows profiles from both the agentihooks repo and any linked bundle.

agentihooks --list-profiles

agentihooks bundle

Manage the linked bundle directory.

agentihooks bundle <action> [path] [--rebase] [--shared | --user]

Subcommands

Subcommand Description
link <path> Link a bundle directory. Stores the path in state.json.
unlink Unlink the current bundle.
list Show the linked bundle path, linked date, and available profiles.
pull Run git pull on the linked bundle directory.
pull --rebase Run git pull --rebase on the linked bundle directory.

Slots

Flag Slot State key Environment fallback
none root bundle bundle AGENTIHOOKS_BUNDLE_PATH
--shared shared bundle, between the root and the user bundle shared_bundle AGENTIHOOKS_SHARED_BUNDLE_PATH
--user personal user bundle, after the shared bundle user_bundle AGENTIHOOKS_USER_BUNDLE_PATH

--shared and --user are mutually exclusive, and one directory fills one slot. Each flag applies to link, unlink, list and pull. Unlinking the shared bundle removes the links it generated and keeps every bundle file. init --force keeps all three links. Content is later-wins in the order root, shared, user; profile names are first-wins in the same order.

Examples

# Link a bundle
agentihooks bundle link ~/dev/my-tools

# Update bundle from remote
agentihooks bundle pull

# Update with rebase
agentihooks bundle pull --rebase

# Show bundle info
agentihooks bundle list

# Unlink
agentihooks bundle unlink

Link an external directory as a chain-able profile. Where bundle link registers a collection of profiles, link-profile registers a single profile dir at any path on disk and (by default) appends it to the active chain.

agentihooks link-profile link <path> [--name <alias>] [--no-append] [--no-init]
agentihooks link-profile unlink <name> [--no-init]
agentihooks link-profile list

Subcommands

Subcommand Description
link <path> Register <path> as a linked profile. Default behavior: append the basename to the active global chain and re-run agentihooks init so settings, CLAUDE.md, rules, and MCP all reflect the new chain.
unlink <name> Remove the linked profile from the registry, strip it from the active chain, sweep any symlinks pointing into it, and re-install.
list Show all linked profiles, their paths, the link date, and which are currently in the chain. Flags missing paths as [MISSING].

Flags

Flag Effect
--name <alias> Use <alias> instead of the directory basename. Required if the basename collides with a built-in or bundle profile (link will refuse otherwise).
--no-append Register the path in state.linked_profiles but do not modify the active chain.
--no-init Update state but skip the immediate re-install. Operator runs agentihooks init later.

Examples

# Link an external profile dir → chain becomes anton,brain → install reapplied
agentihooks link-profile link ~/dev/brain-profile

# Link with explicit alias (avoids collision with built-in)
agentihooks link-profile link ~/dev/anton-fork --name anton2

# Register without touching the chain or running install
agentihooks link-profile link ~/dev/brain --no-append --no-init

# Show all linked profiles
agentihooks link-profile list

# Remove from chain and clean up symlinks
agentihooks link-profile unlink brain-profile

State

Linked profiles live in state.json under a linked_profiles array:

"linked_profiles": [
  {"name": "brain", "path": "/abs/path/brain", "linked_at": "<iso>"}
]

_resolve_profile_dir consults this array as the third lookup tier (after built-in and bundle), so agentihooks init --profile anton,brain works as soon as brain is registered.

Stale paths

If a linked profile’s directory is later deleted from disk, agentihooks init will WARN-skip it and continue with the surviving chain members. The hint message names the exact unlink command:

[WARN] Linked profile 'brain' path is missing — run 'agentihooks link-profile unlink brain' to clean up. Skipping.

agentihooks --query

Print the currently active profile (or chain) and exit.

agentihooks --query

Single profile output:

anton

Chain output:

chain: [coding, anton]

agentihooks status

Show full system health, MCP fleet inventory with real tool counts, and cost guardrails.

agentihooks status

What it checks

Check What it does
Profile Reads state.json for active profile and bundle path
Hooks Parses ~/.claude/settings.json, counts hook event entries (expect 10/10)
Python Extracts the Python binary from hook commands and verifies it runs
Redis Pings Redis, categorizes all agenticore:* keys by type
OTEL Checks if OpenTelemetry hook telemetry is enabled
Guardrails Lists all 8 guardrails with descriptions and enabled/disabled state
Broadcast If BROADCAST_ENABLED=true, reports active session count and pending message count from ~/.agentihooks/broadcast.json. Channel subscriptions aren’t summarised here — they’re visible per-session on statusline Line 3 (read from AGENTIHOOKS_BASE_CHANNELS).
MCP Reads ~/.claude.json for all servers, resolves ${ENV_VAR} auth, queries each HTTP server via MCP protocol for real tool counts, shows fleet total vs active in current project

MCP fleet introspection

The status checker connects to every HTTP MCP server (even disabled ones) to get real tool counts. Auth tokens are resolved from ${ENV_VAR} references in ~/.claude.json headers using env vars loaded by agentienv. Results are cached at ~/.agentihooks/mcp-tool-cache.json with a 1-hour TTL.

The output shows fleet total (all servers) vs active tools (enabled in current project context). Individual servers can be toggled via the /mcp UI — that state is stored in ~/.claude.json’s projects block.

In-session skill

The /agentihooks skill (delivered via the bundle at .claude/skills/agentihooks/) runs the same checker inside a Claude Code session with --session $CLAUDE_SESSION_ID --json, adding live session metrics: context fill %, burn rate, per-tool consumption from the context audit, and warning levels.


agentihooks lint-claude

Analyze a CLAUDE.md file for token cost and suggest sections to extract into on-demand skills.

agentihooks lint-claude [path]

Defaults to ~/.claude/CLAUDE.md if no path is given.

Output

  • Total character and token estimate
  • Per-section breakdown with classification (always-needed vs workflow-specific)
  • Extraction candidates with token savings estimate

agentihooks extract-skill

Extract a section from CLAUDE.md into a standalone skill directory.

agentihooks extract-skill "<Section Heading>" --name <skill-name> [--source <path>] [--output-dir <path>]

Flags

Flag Description
--name Required. Name for the skill directory.
--source Path to CLAUDE.md (default: ~/.claude/CLAUDE.md).
--output-dir Output directory (default: source’s .claude/commands/).

Standalone Python execution

The hook and MCP server modules can be run directly with Python:

# Run the MCP tool server
python -m hooks.mcp

# Run with specific categories
MCP_CATEGORIES=channels,enforcement python -m hooks.mcp

# Process a hook event manually
echo '{"hook_event_name":"SessionStart","session_id":"test-123"}' | python -m hooks

# Pipe a PreToolUse event
echo '{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"ls"}}' | python -m hooks

Exit codes

Code Meaning
0 Success
1 Error (installation failed, missing config, etc.)
2 Block (used by hook handlers to cancel tool execution)

agentihooks swarm wait prerequisite merges

A merge wait beyond the first five queue entries asks the claimant to stack the next change on the prerequisite branch and publish it, or block with the complete prerequisite URL, published stack and next step in the notes. A prerequisite can occupy the task’s current pull request field; that field alone does not establish that the task is finished by its merge.

Confirm a long wait on the task’s final deliverable explicitly:

agentihooks swarm "<slug>" wait --on merge "<pull-request-url>" --final

--final applies only to a merge wait on the task’s current pull request. Use it only when that pull request is the final deliverable. Short queues and unavailable queue metadata preserve the ordinary merge wait.

For a prerequisite, publish the continuation and block with its complete URL in the notes. The block preserves the branch and notes and releases the seat. The tick reopens the matching blocked task after the prerequisite merges. An open, closed unmerged or unreadable prerequisite leaves it blocked. A subsequent checked wait invalidates the earlier continuation offer; unrelated block notes do not consume it.