Codex CLI + 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
- OpenAI
- integration
- rewrites
- hook event
- PreToolUse
- config
- ~/.codex/hooks.json or [hooks] in config.toml
Codex CLI's hook system uses the same event taxonomy and the same payload field names as Claude Code: PreToolUse, a tool_name of Bash for the shell tool, and the command at tool_input.command. That is exactly what tman hook pretooluse reads, so the shipped hook works here with no adapter.
Codex hooks are a comparatively recent, opt-in surface and are not available on Windows. On Windows, and on any version where hooks are disabled, fall back to the PATH shims from step 1 — they are the mechanism that does not depend on a hook surface at all.
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.
npm install -g @standardbeagle/tman # or the install.sh one-liner
cd your-project
tman init --shims --gitignoretman 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
Hooks load from a hooks.json beside an active config layer, or from an inline [hooks] table in config.toml. Either form works; the JSON one is easier to keep identical to your Claude Code setup.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "tman hook pretooluse",
"statusMessage": "routing test commands through tman",
"timeout": 30
}
]
}
]
}
}[[hooks.PreToolUse]]
matcher = "Bash"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "tman hook pretooluse"
timeout = 30Set this at user scope. Codex treats provider, notification, and telemetry keys in a project-local .codex/config.toml as untrusted and warns at startup; keeping the hook in ~/.codex/ avoids that class of surprise entirely. Non-managed hooks also require an explicit trust review the first time they run, so expect one prompt.
The one field to check on your version
Codex documents a rewrite as permissionDecision: "allow" alongside updatedInput. tman emits updatedInput without the accompanying permissionDecision, because that is what the Claude Code contract asks for. Whether your Codex build applies the replacement anyway is the single thing worth confirming before you rely on this — and it is a one-command check:
cd your-project
codex exec 'run the test suite' # then read the transcript
# the command it ran should start with an absolute path to tman.
# if it ran bare, the shims from step 1 are still supervising shell lookups —
# and you can close the gap with the deny adapter from the Cursor guide.Tell the model, in AGENTS.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 AGENTS.md closes that gap:
## 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:
# 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 listThe 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.
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:
| cap | why it matters more under an agent |
|---|---|
--name | The 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 2 | The default, and the right one for most repos. Excess runs queue on a slot file rather than stampeding your cores. |
--max-time | The 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 30m | The built-in. A hang backstop, not a runtime budget: it should sit well above the longest quiet stretch your slowest command legitimately has. |
--max-mem | Opt-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 Codex CLI name its shell tool `Bash` or `shell`?
`Bash`. Codex's PreToolUse hook intercepts the shell tool under the tool name `Bash`, matching Claude Code's naming, which is why tman's shipped hook — which requires exactly that name — works unmodified.
Codex hooks are not available on Windows. What supervises there?
The PATH shims written by `tman init --shims`. They catch any command that goes through a shell lookup, which covers everything you type and everything a script invokes, and they do not depend on a hook surface existing.
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.