Claude Code + tman

The agent's pre-tool hook can replace the command before it runs, so a bare npm test becomes a supervised one with no cooperation from the model.

vendor
Anthropic
integration
rewrites
hook event
PreToolUse
config
.claude/settings.json

Claude Code is the integration tman was built against, and the only one that needs no adapter: tman hook pretooluse speaks its PreToolUse contract directly. Register it once and every bare npm test the agent issues comes back rewritten as a supervised run.

1. Install tman and adopt the project

Everything below assumes the binary is on your PATH and the repository has a .tman.kdl. That file is what scopes caps to this project, and it is also the signal several of the integrations test for before they do anything.

shell
npm install -g @standardbeagle/tman   # or the install.sh one-liner

cd your-project
tman init --shims --gitignore

tman init writes a defaults block and an alias for each test or build command it can detect. Commands it cannot detect are left commented out rather than stubbed, so an alias you never filled in fails loudly instead of exiting 0 having run nothing.

2. Register the PreToolUse hook

Project scope is .claude/settings.json; user scope is ~/.claude/settings.json and covers every repository at once. The matcher is the Bash tool, because that is the only tool that can start a process.

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "tman hook pretooluse" }]
      }
    ]
  }
}

What the hook does to each command

the agent runswhat happens
go test ./... in a project with .tman.kdlrewritten to /abs/path/to/tman run -- go test ./..., and the rewrite is announced in the transcript
go test ./... with no .tman.kdlruns unchanged; the model is told it was unsupervised and how to adopt
tman test, or anything inside a supervised treeuntouched — TMAN_RUN_ID is set, so this is the same work as its parent and gets no second supervisor
cd app && npm test, CI=1 npm testruns unchanged, with a note. The hook does not parse shell and will not prefix a string it did not parse
git status, npm run devuntouched
any of the above, when tman cannot prove which binary it is running asruns unchanged, with a note naming the path it refused to use

It cannot block you. Exit code 2 is the only status Claude Code treats as a block, and this hook never returns it. A missing binary, a malformed request, an unreadable project: every failure path leaves the command exactly as written. Losing supervision costs you a backstop; it never costs you the build.

Why the rewrite names an absolute path

The rewritten command starts with the running binary's full path, not the word tman. The Bash tool resolves a program name against its own PATH, which frequently does not include ~/.local/bin or the npm bin directory tman installs into — so a bare name would turn a working build into command not found.

That path has to be proven to be tman before it is emitted: fully qualified, named as tman ships, and present on disk. Launched as dotnet tman.dll, the running process is the shared dotnet host — rewriting to it would hand the agent dotnet run -- go test ./..., which in a directory holding a .csproj builds and launches an unrelated application. When tman cannot prove what it is about to name, it warns and changes nothing.

Tell the model, in CLAUDE.md

Shims and hooks catch the mechanical cases. The remaining gap is the model deciding to do something clever — running the suite through a subshell, or with an inline environment assignment, both of which tman deliberately refuses to rewrite because prefixing a string it did not parse changes what runs. A standing instruction in CLAUDE.md closes that gap:

CLAUDE.md
## Running tests and builds

Run every test, build, lint, and typecheck command through tman — never bare.
Prefer the repo-root shims (`./test`, `./lint`) when they exist; they carry
this project's `.tman.kdl` caps. Otherwise prefix explicitly:

    tman run -- <command>

Never widen the caps from the command line. Exit 124 is a timeout, 125 a stall,
126 a resource cull — report those, do not retry with looser limits.

Verify it is actually on

A supervision integration that silently does nothing is worse than none, because it stops you looking. Two checks, both of which fail visibly:

shell
# 1. the classifier agrees this command is worth supervising
echo '{"tool_name":"Bash","cwd":"'"$PWD"'","tool_input":{"command":"npm test"}}' \
  | tman hook pretooluse

# 2. a real run shows up while it is in flight
tman run --name smoke -- sleep 30 &
tman list

The first prints a JSON object naming the rewritten command. Empty output means tman decided to stay out of the way — no .tman.kdl above cwd, a command it does not classify as test or build work, or a shell string it refused to parse. The second must list the run; if tman list is empty, nothing is being supervised.

Inside Claude Code, /hooks lists what is registered, and a supervised run announces itself in the transcript — if you see the command run with no such line, the hook is not firing.

Caps worth setting for agent-driven work

An agent re-runs the same command far more often than a human does, and it does it while you are not watching. That changes which caps earn their keep:

capwhy it matters more under an agent
--nameThe agent will start the suite again before the last one finished. A dedup lock refuses the duplicate instead of running two copies against the same port or database.
--max-parallel 2The default, and the right one for most repos. Excess runs queue on a slot file rather than stampeding your cores.
--max-timeThe only cap that bounds a run which is genuinely working but will never finish — a wait on a peer that never answers looks identical to healthy IO.
--stall 30mThe built-in. A hang backstop, not a runtime budget: it should sit well above the longest quiet stretch your slowest command legitimately has.
--max-memOpt-in, and worth it where a leak mode exists — worker pools, browser fleets, long-lived daemons. Off by default because builds are supposed to be greedy.

Per-command starting points live in the tuning guides — one page per test and lint tool, with the reasoning behind each number rather than just the number.

Frequently asked

Does the tman hook ever block Claude Code from running a command?

No. Exit code 2 is the only status Claude Code treats as a block, and the hook never returns it. Every failure path — missing binary, malformed request, unreadable project — leaves the command exactly as written, so an uninstalled tman costs supervision but never the build.

Why does the hook skip commands like `cd app && npm test`?

The hook does not parse shell. Prefixing a chained or environment-prefixed string would change what runs — `tman run -- cd app && npm test` supervises the `cd`, not the test. Rather than guess, tman leaves the command untouched and tells the model it was unsupervised.

Do I still need PATH shims if the hook is registered?

They cover different paths. The hook catches commands the agent issues through its Bash tool; the shims catch commands that go through a shell lookup, including ones you type yourself and ones a script invokes. Running both costs nothing: a command already inside a supervised tree sets TMAN_RUN_ID and is never supervised twice.

Other agents

Sources

Vendor documentation this guide is written against — hook surfaces move, so check yours if something below does not match what you see.