Gemini 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
Google
integration
rewrites
hook event
BeforeTool
config
.gemini/settings.json

Gemini CLI's pre-execution event is BeforeTool, and it can replace a command: the object at hookSpecificOutput.tool_input merges with and overrides the model's arguments before the tool runs. The names differ from tman's native output, so this integration needs a short wrapper — but the capability is the same one Claude Code and Codex have.

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. Add the BeforeTool wrapper

The wrapper does two translations: Gemini's tool name and argument shape on the way in, and updatedInput to tool_input on the way out. It reuses tman's classifier rather than reimplementing it, so which commands count as supervisable stays in one place.

~/.gemini/tman-gemini-hook.sh — chmod +x
#!/usr/bin/env bash
# tman-gemini-hook.sh — adapt tman's PreToolUse output to Gemini's BeforeTool contract.
set -euo pipefail
payload=$(cat)

command=$(jq -r '.tool_input.command // empty' <<<"$payload")
cwd=$(jq -r '.cwd // empty' <<<"$payload")
[ -n "$command" ] || exit 0

verdict=$(jq -nc --arg c "$command" --arg d "${cwd:-$PWD}" \
  '{tool_name:"Bash", cwd:$d, tool_input:{command:$c}}' | tman hook pretooluse)
[ -n "$verdict" ] || exit 0

supervised=$(jq -r '.hookSpecificOutput.updatedInput.command // empty' <<<"$verdict")
if [ -n "$supervised" ]; then
  jq -nc --arg c "$supervised" --arg m "$(jq -r '.systemMessage // ""' <<<"$verdict")" \
    '{systemMessage:$m, hookSpecificOutput:{tool_input:{command:$c}}}'
else
  # advice only — pass the note to the model, change nothing.
  jq -c '{systemMessage: (.hookSpecificOutput.additionalContext // "")}' <<<"$verdict"
fi

3. Register it against the shell tool

For BeforeTool, the matcher is compared against the name of the tool being executed, and it is a regular expression. Gemini's shell tool is run_shell_command.

.gemini/settings.json (or ~/.gemini/settings.json for every project)
{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "run_shell_command",
        "hooks": [
          {
            "type": "command",
            "name": "tman supervision",
            "command": "~/.gemini/tman-gemini-hook.sh",
            "timeout": 30000
          }
        ]
      }
    ]
  }
}

The timeout is in milliseconds here — the default is 60000. A value of 30, copied from a Codex or Claude config where the unit is seconds, gives the hook 30ms and it will appear to do nothing at all.

Tell the model, in GEMINI.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 GEMINI.md closes that gap:

GEMINI.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.

Run the wrapper by hand first — it is a plain filter, so it needs no agent to test: echo '{"tool_input":{"command":"npm test"},"cwd":"'"$PWD"'"}' | ~/.gemini/tman-gemini-hook.sh. It should print an object containing hookSpecificOutput.tool_input.command.

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

Why does Gemini CLI need a wrapper when Codex does not?

Codex uses the same field names as Claude Code — `updatedInput` under `hookSpecificOutput`. Gemini merges a `tool_input` object instead, and matches on `run_shell_command` rather than `Bash`. The capability is identical; only the names differ, so the wrapper is a translation and not a second classifier.

Is the hook timeout in seconds or milliseconds?

Milliseconds, with a default of 60000. This differs from Codex and Claude Code, where hook timeouts are seconds — a config copied across without changing the unit gives the hook 30ms and it silently never completes.

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.