what your agent sees
asking mid-plan
The hooks speak at the last useful moment — the edit itself. The MCP server (hooks/mcp_server.mjs, committed beside the hooks) exposes the same room over stdio, so an agent can ask while it is still deciding what to do:
| Tool | What it answers |
|---|---|
who_is_working_on_what | Who else is active in this repo, what they said they're doing, their branch, the files they've touched |
has_anyone_touched | Whether anyone has recently edited a given file — or anything under a given directory |
list_contested_files | Which files more than one person's agent has edited recently |
claim_work | Say what you are about to work on — paths and a one-line task — and learn, before writing, whether anyone has claimed part of it |
release_claim | Let go of a claim when the work is done |
list_claims | What other people's agents have said they are taking right now |
It is a client of the coordination server, not a second server: it reads GET /activity
and GET /claims, and writes claims through POST /claims, all with the room id the hooks
already resolve from .claude/agentmash/config.json. No new credential, nothing else to keep
in sync.
How it reaches your teammates
agentmash init wires it up; a teammate does nothing but git pull. The server travels the
way the hooks do: mcp_server.mjs, its two helpers and mcp_launcher.mjs are committed in
.claude/agentmash/ beside them, and like them they are plain Node, with nothing to install.
.mcp.json, committed too, registers it:
// .mcp.json — project-scoped MCP servers, at the repo root
{
"mcpServers": {
"agentmash": {
"command": "node",
"args": [
"--input-type=module",
"-e",
"import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; const start = path.resolve(process.env.CLAUDE_PROJECT_DIR ?? ''); for (let dir = start; ; dir = path.dirname(dir)) { const launcher = path.join(dir, '.claude', 'agentmash', 'mcp_launcher.mjs'); if (fs.existsSync(launcher)) { await import(pathToFileURL(launcher).href); break; } let registers = false; try { registers = fs.readFileSync(path.join(dir, '.mcp.json'), 'utf8').includes('mcp_launcher'); } catch {} let repoRoot = false; try { repoRoot = fs.statSync(path.join(dir, '.git')).isDirectory(); } catch {} if (registers || repoRoot || path.dirname(dir) === dir) { console.error('[agentmash-mcp] no .claude/agentmash/mcp_launcher.mjs from ' + start + ' up to ' + dir + ': run npx agentmash init'); process.exit(1); } }"
]
}
}
}Claude Code reads .mcp.json from the folder a session starts in and from every folder above
it, and starts the server in that folder: the repo's root, or a subfolder of it just as
often. So the entry is a few statements of Node that do the same from anywhere — walk up from
that folder to the first .claude/agentmash/mcp_launcher.mjs and start it — and the launcher
reads the room from the repository it is installed in. There is nothing in it for Claude Code
to expand. The walk stops at the folder whose .mcp.json registered the server, or at a
repository's root (a .git folder), whichever comes first: a launcher above those is code
nobody in the repository put there — on Windows any local user can make a
C:\.claude\agentmash\ — so a repository whose .claude/agentmash/ is missing fails, and
names the folder it started in, rather than running someone else's. A worktree's .git is a
file, and the walk goes past it, so a session in a worktree Claude Code made in
.claude/worktrees/ still finds the main checkout's launcher when its own commit predates the
install. The entry it replaces, the launcher's path behind ${CLAUDE_PROJECT_DIR:-.},
looked right and is the form the Claude Code docs name, but Claude Code expands it from its own
environment, where that variable is unset, so it always meant ./.claude/agentmash/ from the
session's folder: from a subfolder, a launcher that is not there and no tools at all. Running
npx agentmash init again rewrites an entry of that shape. agentmash doctor starts the
command out of .mcp.json, from the root and from a subfolder, so a wrong belief about any of
this fails out loud.
What AgentMash does not write: enabledMcpjsonServers. Claude Code asks each person to
approve a project's MCP servers the first time, and that allow-list is how the prompt is
skipped. Putting agentmash in it and committing it would answer that prompt, for a server
that starts a process, on behalf of everyone who later pulls the commit — one person running
init is not the rest of the team consenting. (Claude Code guards this too: an approval
committed to a repo's own .claude/settings.json is ignored until you trust the folder
yourself.) So each person approves once, on the machine they own. The cost is one dialog;
the alternative is a repo that can pre-authorise code execution on everyone who clones it.
Why the server is committed, and what the launcher is for. The hooks travel by git pull
because they are plain Node with no dependencies. The MCP server used to be built on
@modelcontextprotocol/sdk, which a teammate's checkout has no reason to contain, so
everywhere but an AgentMash checkout it could only start as a stand-in that says it cannot
answer. It now speaks MCP's newline-delimited JSON-RPC itself, and the tests drive it with the
SDK's own client so the two cannot drift. A registered server that fails to start is
invisible to the agent: the tools are simply absent, so it stops asking and carries on as
though nobody else were working in the repo — the one wrong answer this project exists to
prevent. So what .mcp.json starts is mcp_launcher.mjs, which finds a server in three
layers and never fails silently:
| What a teammate sees | ||
|---|---|---|
| 1 | The server committed beside it, imported in-process — or, when this machine's npx agentmash login keeps a newer copy in ~/.agentmash/hooks, that one, so a repo that ran init months ago does not hold a developer back. An AgentMash checkout commits the launcher and no server, and gets its source, hooks/mcp_server.mjs. AGENTMASH_MCP_ENTRY comes before all of them | the tools, immediately |
| 2 | npx agentmash mcp, which fetches the package once and caches it — off unless this machine sets AGENTMASH_MCP_NPX=1, because downloading and running code from the registry at session start is the machine owner's call | the tools, ~12 s the first time and ~2 s after |
| 3 | A degraded server implemented inside the launcher, in plain JSON-RPC, for a server file that is missing or will not load | the tools are there and answer, and every answer says AgentMash could not start and the result is UNKNOWN |
Layer 3 is why the launcher is more than an import. It starts on a machine with nothing
installed and no network, answers tools/list with the same three tool names, and returns
this from every call, as an error result:
AgentMash could not start its MCP server on this machine, so it cannot say who is
working in this repository.
Treat the answer as UNKNOWN. This is not the same as "nobody is working on anything" —
AgentMash never got to ask.
Reason: the MCP server is missing: there is no mcp_server.mjs beside this launcher in .claude/agentmash/The hooks are unaffected either way: edits are still reported and teammates still get warned before they edit a file someone just changed. Only the query tools go missing, and they say so.
agentmash doctor checks all of it — that the launcher and its server are installed, that
.mcp.json registers them, that the server is approved, and that it really starts and
answers a tools/list, from the root and from a subfolder:
✓ MCP launcher present, with its server .claude/agentmash/mcp_server.mjs 0.10.0
✓ MCP server registered in .mcp.json
· Claude Code will ask you once to approve the agentmash MCP server in .mcp.json.
Until you do, its tools are absent from your sessions — the hooks are unaffected.
✓ the command in .mcp.json starts and answers tools/list 8 tools · 0.10.0 · 114ms
· and from src/ too, where a session started in a subfolder runs it
· node --input-type=module -e "import fs from 'node:fs'; import path from 'node:path'; import { path..."That check runs the command out of .mcp.json, expanded the way Claude Code expands it,
rather than the launcher's resolved path — otherwise it would prove a path no teammate takes.
When the server is missing from beside the launcher:
✗ MCP server started in degraded mode — its tools answer "unavailable" instead of reporting
who is working. [agentmash-mcp] degraded: the MCP server is missing: there is no mcp_server.mjs beside this launcher in .claude/agentmash/
Fix: run agentmash init here and commit .claude. The server is plain Node, committed
beside the launcher, so every teammate has it after their next pull.This repo installs into itself, so an agent working on AgentMash runs the same launcher a
teammate does — and a test asserts the committed copy matches hooks/mcp_launcher.mjs.
Neither the version layer 2 asks for nor the version the server says it is is typed in by
hand: cli/sync-hooks.mjs stamps the package's own version into the launcher and the server
at pack time, so the copies committed into a repo always name the version they shipped with,
and a missing marker fails the pack instead of shipping a pin nobody published.