{
"name": "ct-steer",
"description": "Steer ad-hoc shell to the ct tool that serves it, and install the steering hook. ct-steer recognises a fixed set of high-confidence shell idioms a suite tool serves better — `find … | xargs grep` / `find -exec grep` and `grep -r`/`rg`/`ag` (ct search), `find … -name` (ct search), `sed -i`/`perl -i` (ct edit), `head`/`tail`/`sed -n 'A,Bp'` on a file and `python -c`/`node -e`/`perl -e`/`ruby -e`/`jq` reading a file (ct view / ct search), `grep -c` (ct search --summary), `ls -R`/`tree` and `wc`/`wc -l` over files incl. `cat FILES | wc` (ct tree), `for`/`while` per-item loops (ct each), sleep-bearing `for`/`while`/`until` poll/wait loops (ct await), and `&&`/`||` chains whose every segment is itself ct-serviceable (ct and / ct or). A multi-line scriptlet is classified line by line (ignoring cd/assignments/echo/comments): when every meaningful step is already ct or ct-advisable and not all are yet ct, the whole thing folds into one shell-less `ct and A ::: B ::: C` chain; when some steps are opaque, the ct equivalents are advised individually. Run as a Claude Code PreToolUse hook, it steers the agent to the ct equivalent instead of running the raw command. The matcher is deliberately conservative: it only fires on those idioms, never re-steers a command that already invokes ct, and the hook is fail-open (anything it does not recognise, or any malformed input, is allowed silently). Beyond Bash, the hook can also gate the harness's own Grep/Glob/Read tools — Grep/Glob steer to ct search, Read to ct view (images/PDFs/notebooks pass through) — enabled per tool at install time. Subcommands: `hook` is the runtime hook — it reads a PreToolUse tool-call envelope on stdin and prints a decision JSON (`permissionDecision` deny/ask, or an `additionalContext` warn) on a match, nothing on a miss, always exiting 0 so it never fails the call on its own account; `--mode deny|ask|warn` (default deny) picks the action. `install`/`uninstall` merge or remove the PreToolUse steering hook in a Claude Code settings file (`--scope project|local|user`, default project → .claude/settings.json / .claude/settings.local.json / ~/.claude/settings.json; `--tools Bash,Grep,Glob,Read` (default Bash) chooses which tools to gate, one matcher entry each); the merge is idempotent and preserves the rest of the file — including comments and layout — because it edits through ct-patch's byte-range splices rather than reserialising, with `--dry-run` to show the resulting file and `--print` to emit just the snippet for manual paste. `check` classifies a command string and prints what the hook would decide, exiting 0 when the command is allowed and 1 when it would be steered. Invoke as `ct steer …` or `ct-steer …`.",
"input_schema": {
"type": "object",
"properties": {
"json": {
"type": "boolean",
"description": "Emit a structured JSON result instead of text where applicable (the install/uninstall outcome, or the check decision)."
},
"quiet": {
"type": "boolean",
"description": "Suppress informational output (exit status still reports)."
},
"timeout": {
"type": "number",
"description": "Abort with exit 2 if the run exceeds SECS seconds (fractional allowed)."
},
"heartbeat": {
"type": "number",
"description": "Print a liveness pulse every SECS seconds (fractional allowed) while running."
},
"heartbeat-emit": {
"type": "string",
"description": "Heartbeat line template. Tokens: {ELAPSED} {TOOL}. Default: \"[{ELAPSED}s]\"."
},
"heartbeat-to": {
"type": "string",
"enum": [
"stderr",
"stdout"
],
"description": "Stream heartbeat pulses are written to. Default: stderr."
}
}
},
"commands": [
{
"name": "hook",
"description": "Runtime PreToolUse hook: read a tool-call envelope on stdin, print a deny/ask/warn decision on a match (nothing on a miss). Handles Bash plus the harness Grep/Glob/Read tools. Always exits 0. --mode deny|ask|warn (default deny). --nudge-pipelines adds a warn-only (never deny) nudge against ANY shell pipeline the specific rules did not steer, prompting harder use of ct. Tool-call logging is ON by default: it appends one JSONL record per call — including the silent allows, the raw material for spotting un-steered idioms — to .ct/tclog/<yyyy-mm-dd>.jsonl under the nearest .ct (created if absent), and keeps a .ct/.gitignore '*log' rule so the logs stay out of git. Each record has event ('pre'), tool, command, cwd, session_id, decision, rule_id, ct_tool, ts_ms. --log-dir DIR (or CT_STEER_LOG) redirects the logs; --no-log disables them. Best-effort and fail-open."
},
{
"name": "post",
"description": "Runtime PostToolUse recorder: read a PostToolUse envelope on stdin and append a record of the call as it actually executed (event 'post', with tool, command, ct (whether the executed command used ct), cwd, session_id, ts_ms) to the same daily .ct/tclog log. Enables measuring whether steer guidance was followed — correlate a 'pre' steer decision with the follow-up 'post' call by session_id and time. Only observes; always exits 0. Shares --log-dir/--no-log with the hook. Wired by `install --measure`."
},
{
"name": "install",
"description": "Merge the PreToolUse steering hook into a Claude Code settings file. --scope project|local|user (default project), --mode deny|ask|warn, --tools Bash,Grep,Glob,Read (default Bash; one matcher per tool), --all-tools (a single '*' matcher, supersedes --tools; for full-coverage logging), --nudge-pipelines (bake the warn-only pipeline nudge into the hook), --measure (also install a PostToolUse '*' recorder running `ct steer post`, to measure whether guidance was followed), --log-dir DIR (bake a log-directory override; logging is on by default), --no-log (bake in disabling the default tool-call logging), --dry-run (show the file), --print (emit just the snippet). Before a real write, a preflight verifies the `ct` that will fire the hook can parse the armed command (subcommand + baked flags) — a hook the resolving `ct` rejects would clap-error and block tool calls; install REFUSES (exit non-zero, with a recovery hint) if that check fails. --force skips the preflight; --pin bakes THIS binary's absolute path into the hook (instead of resolving `ct` on PATH) so version skew or a missing `ct` can't break it. If a hook ever starts erroring, `ct steer uninstall` (which uses only stable syntax) clears it. Idempotent."
},
{
"name": "uninstall",
"description": "Remove the steering hook from a Claude Code settings file, pruning emptied entries. --scope project|local|user, --dry-run."
},
{
"name": "check",
"description": "Classify a COMMAND string and print what the hook would decide. Exit 0 when allowed, 1 when it would be steered. --mode affects the printed label; --json prints the decision JSON."
}
],
"examples": [
{"cmd": "ct steer install --all-tools --mode warn", "why": "Log every tool call to .ct/tclog and non-intrusively steer recognized idioms."},
{"cmd": "ct steer check 'grep -r TODO src'", "why": "Ask what the hook would do with a command (exit 1 means it would steer to ct)."}
]
}