Getting it into your agent
One page per mod, every tool's command on it. A separate URL per tool would split the same page into five that compete with each other.
npx agentmods add instructions/ginkida/agent-dispatch/agents-mdgit clone --depth 1 https://github.com/ginkida/agent-dispatchWhat it costs to keep this loaded
Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.
| Model | Per session | Once invoked |
|---|---|---|
| Fable 5 | $0.03345 | $0.03345 |
| Opus 5 | $0.01673 | $0.01673 |
| Sonnet 5 | $0.00669 | $0.00669 |
| Haiku 4.5 | $0.00334 | $0.00334 |
Grade A, and why
agent-dispatch AGENTS.md scanned grade A with 1 finding against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 2d ago.
A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.
Runs shell commandslowCapability
Expected in a hook, worth knowing in a rule or an instructions file.
Tests must **never** invoke the real `claude` CLI. Runner tests mock `shutil.which` + `subprocess.run`/`Popen`; server tests mock `_get_config` + `runner.dispatch`. The one exception is `TestStreamPipeHandling`, which sp How it starts
The opening of the file, as written. The whole thing — 89 lines — stays where its author put it; the contents beside it link to each section on GitHub.
AGENTS.md
Guidance for AI coding agents working on this repository.
Using agent-dispatch (not developing it)? Read README.md — it has the full setup path with verify steps and the complete MCP tool reference. This file is for contributing to the codebase.
What this project is
MCP server + CLI that lets Claude Code agents delegate tasks to agents in other project directories. One sync core, two surfaces:
| File | Role |
|---|---|
src/agent_dispatch/runner.py |
Sync subprocess wrapper around claude -p — the actual work |
src/agent_dispatch/server.py |
Async FastMCP interface (21 MCP tools), wraps runner in asyncio.to_thread + semaphore |
src/agent_dispatch/cli.py |
Click CLI: init, add, update, remove, list, describe, test, doctor, jobs, job, cancel, gc, group (add/list/inspect/update/remove), serve |
src/agent_dispatch/models.py |
Pydantic v2 models (AgentConfig, DispatchGroup/GroupMember, Settings, DispatchResult) |
src/agent_dispatch/config.py |
YAML config load/save + project auto-description |
src/agent_dispatch/cache.py |
Thread-safe in-memory TTL cache |
src/agent_dispatch/jobs.py |
Persistent per-job JSON files for async dispatch |
Dev setup
pip install -e ".[dev]"
Gates — both must pass before a change is done (CI rejects otherwise)
ruff check src/ tests/
python3 -m pytest tests/ -v # 578 tests, ~5s
Tests must never invoke the real claude CLI. Runner tests mock shutil.which + subprocess.run/Popen; server tests mock _get_config + runner.dispatch. The one exception is TestStreamPipeHandling, which spawns a short-lived python subprocess: a pipe deadlock lives in the OS pipe buffer, so a mocked Popen structurally cannot reproduce it.
Non-obvious invariants (violating these breaks real behavior)
allowed_tools/disallowed_toolsare tri-state:None= inherit settings defaults,[]= explicitly no tools,[...]= exactly these. Check withis not None, neveror—[]is falsy but semantically distinct.- Error-type precedence on an
is_errorpayload (_build_error_result): the CLI's own budget stop wins, thendenied_toolsnon-empty ⇒error_type="permission"regardless of the error text, then text classification. - Groups: a group's
shared_contextis folded into thecontextstring before the cache/runner calls (_merge_group_contextin server.py) — runner.py and cache.py are untouched, the cache key disambiguates groups for free, andgroup=""is byte-identical to a plain dispatch. Membership is validated up front (_validate_group_member, separate from the pure merge sodispatch_parallel's all-or-nothing pre-check holds).DispatchConfigvalidates only group keys, never member existence — a hard cross-ref check would brick config load when a shared gateway agent is removed; dangling refs are flagged (unknown:true) at read time instead. - On failure, callers read
DispatchResult.error+error_type—resultholds the raw agent output even on errors. --session-idand--resumeconflict — never pass both toclaude.- Valid permission modes:
default,plan,bypassPermissions(models.py: KNOWN_PERMISSION_MODES). JobStore.finish/failrefuse already-terminal jobs (returnsNone) — this closes the race with force-cancel; never "fix" it by overwriting.mark_runninglikewise refuses any job that isn'tpending, so a stale or duplicate worker can't resurrect a finished one.- "Is this group member missing?" has exactly one implementation:
DispatchConfig.unknown_group_members(). Any new surface that lists or validates membership calls it instead of re-deriving the check. - Cancelling a running job requires the in-memory
_running_procsregistry (server.py) — the job is markedcancelledbefore the subprocess is killed. Don't persist PIDs to disk (PID reuse after restart could kill an unrelated process). max_budget_usdis enforced by the claude CLI (_build_commandpasses--max-budget-usd): a run stopped at the cap comes backis_errorwith noresulttext, and_build_error_resultturns it intoerror_type="budget"+budget_exceeded=True+ a resumablesession_id._apply_budgetis the secondary, post-hoc signal for an overshoot that didn't stop the run; it never fails a dispatch.- A CLI error payload can have no
resultfield at all — the reason lives inerrors/subtype. Read it via_cli_error_details, never assumeresultis populated on failure. - Both subprocess pipes must be drained concurrently.
dispatch_streamreads stdout in a loop while a daemon thread drains stderr; reading stderr only after the loop deadlocks any child that writes more than ~64 KiB to it (the child blocks inwrite(2), never emits its result, and the dispatch dies at the timeout).dispatch()is immune only becausesubprocess.run(capture_output=True)usescommunicate(). - A received
resultevent outranks the timeout flag. The agent finished and was billed; a lingering process is a cleanup problem, reported as ahint, not a failure. - The timeout must kill the process tree, not the child.
dispatch_streamspawns withstart_new_session=Trueand_kill_process_treesends SIGKILL to the group: a grandchild that inherited stdout keeps the read loop parked long past the deadline otherwise.killpgis guarded on a positive pid —killpg(0)would signal the dispatcher's own group. - Never
close()a pipe another thread may still be reading.close()waits on the reader's buffer lock with no timeout, so it would hang the dispatch forever — the boundedjoin()before it buys nothing.dispatch_streamskips the stderr close while the drain thread is alive and lets the daemon reader + Popen finalizer release the fd. This is not hypothetical: a stdio MCP server inherits the child'sstderr, so the pipe often has no EOF even afterclaudeexits cleanly. - No
awaitinsideconfig_lock().ProcessLock's in-process guard is athreading.RLock— re-entrant per thread — and every MCP tool coroutine runs on the one event-loop thread. Suspending in the critical section lets a second coroutine re-enter the "held" lock and interleave its own load/mutate/save. Collect warnings as data, emit them after thewithblock (test_no_await_inside_the_config_lockenforces this by AST). - Cross-process locks are acquired with a bounded wait, never a blocking
flock: the server takes them on its event-loop thread, so a wedged holder would freeze every tool. After the deadline it proceeds unlocked and logs — a possible lost update beats a permanent freeze. recover_stalesweepspendingon a much longer threshold thanrunning: the jobs directory is shared by everyagent-dispatch serve, so an hours-old pending job may still be queued behind another live server's semaphore. For a running job,started_atalone does not prove abandonment — a dispatch may legitimately run to the 7200s timeout ceiling, so the file's own mtime is checked too (a live worker rewrites it on every progress flush). That check can only ever skip a recovery, never add one.- Deleting job records is opt-in (
settings.job_retention_days, default0= off) and only ever happens at server start, never inside a tool. They are the user's dispatch history and the deletion is irreversible, so an unreadable config is treated as "do nothing" rather than falling back to a default retention. max_concurrencymust bound subprocesses, not coroutines. Cancelling a coroutine does not stop the thread behindasyncio.to_thread, soasync with sem:gave the slot away whileclaudekept running and billing. Dispatches go through_dispatch_guarded, which releases from the future's done-callback andshields the await. Never "simplify" it back toasync with.- Tool responses are serialized through
server._dumps, never barejson.dumps: the stdlib default (ensure_ascii=True) turns every non-ASCII character into a\uXXXXescape, tripling the bytes and tokenizing badly, for no gain — the stdio transport emits raw UTF-8 via pydantic anyway. A reallist_groups()carried 8520 escapes and weighed 59 KB instead of 25 KB. agents.yamlis parsed with libyaml'sCSafeLoaderwhen available (config._YamlLoader), falling back to the pure-Python safe loader — neveryaml.Loader. The config is re-read on every tool call, so this parse is on the hot path of all 21 tools and blocks the event-loop thread: 9.80 ms → 0.76 ms on a real 38 KB config.- Pydantic does not validate on assignment.
Field(ge=...)guards only the load path; every mutation surface (CLIadd/update, MCPadd_agent/update_agent) needs its own boundary check, or the bound escapes as a rawValidationError. - Every state file (
agents.yaml, job files) is written temp file +os.replace, never in place, and every load/mutate/save is wrapped inconfig.ProcessLock— the CLI and the MCP server are separate processes writing the same files, so a thread lock alone loses updates. - Anything that changes an agent's config must call
_invalidate_agent_cache— the cache key holds the agent name, not its directory or permissions. - Only clean successes are cached:
cache.putrefuses failures,denied_toolsresults, andbudget_exceededresults, so the documented "grant access, then re-dispatch" recovery is never short-circuited. - Remediation text is a contract: a hint that names a flag must name one that exists (
test_printed_budget_hint_is_a_runnable_commandfeeds the printed flags back into the CLI). Run the command you print. - The config error sets are declared once and in two halves:
config.CONFIG_LOAD_ERRORS(read) andconfig.CONFIG_SAVE_ERRORS(write —yaml.dump'sRepresenterErroris ayaml.YAMLError, therefore neitherOSErrornorConfigLoadError, and used to escape both the MCP guard and the CLI's_save_or_exit). Two halves, not one set, because the remediations differ: a failed write is atomic so the old config survives, while a failed read needs the YAML fixed. - MCP tools that load config carry
@_config_guardunder@mcp.tool()so a brokenagents.yaml— or a failed write — returns the{"error": ...}envelope instead of a raw traceback. The set of load errors lives in one place (config.CONFIG_LOAD_ERRORS) because three surfaces handle it:UnicodeDecodeErroris aValueError, not anOSError, and listing types per-site is exactly how a cp1251 config slipped past all three.
What this file has done since we first saw it
Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.
- 2d ago First seen · 89 lines · 3,345 tokens per session scan A 1cd79c31df72
agent-dispatch AGENTS.md is an instructions file published in the GitHub repository ginkida/agent-dispatch (29 stars, last pushed 19d ago), licensed MIT. It adds 3,345 tokens to every session, about $0.0167 per session on Opus 5. A static security scan graded it A with 1 finding (runs shell commands). No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-30.
Other instructions, from other repositories
semantix CLAUDE.md
Instructions for Gnosil/semantix, covering 项目规则(claude code) and 1. 改动须经用户过目;由 claude 提交到分支 + pr,用户 review 后合并.
rn-dev-agent AGENTS.md
Instructions for Lykhoyda/rn-dev-agent, covering repository guide for agents, repository map, editing rules, architecture rules and supported node runtimes.
hindsight CLAUDE.md
Instructions for vectorize-io/hindsight, covering claude.md, project overview, development commands, local development (api + ui) and start both api server and control plane ui.
codedb AGENTS.md
Instructions for justrach/codedb, covering codedb agent guidelines, what codedb is (and isn't), review guidelines, pre-merge verification and security-sensitive areas.
claude-code-settings copilot-instructions.md
Instructions for feiskyer/claude-code-settings, covering claude.md, environment setup, required dependencies, configuration and skills.
open-bridge AGENTS.md
Instructions for bks-lab/open-bridge, covering the bridge — agent instructions, required reading, session start detection (automatic), theme and agents.