LLM Agent Hooks
Scope
Deployed hook artifacts — the files and agent-specific configuration installed on user machines by rtk init. External scripts and plugins are thin delegates to rtk rewrite; native processors such as Codex call the same Rust rewrite registry directly. Zero filtering logic is duplicated in this directory.
Owns: per-agent hook scripts and configuration files for 12 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode, Hermes, Pi, Oh My Pi, Mistral Vibe, Trae).
Does not own: hook installation/uninstallation (that's src/hooks/init.rs), the rewrite pattern registry (that's discover/registry), or integrity verification (that's src/hooks/integrity.rs).
Relationship to src/hooks/: that component creates these files; this directory contains them.
Awareness files
rtk-awareness.md, rtk-awareness-high.md, rtk-awareness-full.md are the agent-neutral
instruction files embedded by rtk init (RTK.md, GEMINI.md, rules files, AGENTS.md blocks).
awareness.level in config.toml picks one for hook-based agents; agents without a hook always get
rtk-awareness-full.md, the only one that tells the agent to prefix commands with rtk.
Purpose
LLM agent integrations that intercept CLI commands and route them through RTK for token optimization. Each hook transparently rewrites raw commands (e.g., git status) to their RTK equivalents (e.g., rtk git status), cutting up to 90% of the bash output that reaches the LLM context without requiring the agent or user to change their workflow.
How It Works
Agent runs command (e.g., "cargo test --nocapture")
-> Hook intercepts (PreToolUse / plugin event)
-> Reads JSON input, extracts command string
-> Uses the shared RTK rewrite registry
-> Registry matches pattern, returns "rtk cargo test --nocapture"
-> Hook sends response in agent-specific JSON format
-> Agent executes "rtk cargo test --nocapture" instead
-> Filtered output reaches LLM (up to 90% fewer bash output bytes)
All rewrite logic lives in the Rust binary (src/discover/registry.rs). External hook scripts call rtk rewrite; native Rust hook processors call the registry directly. This keeps one source of truth for all 70+ rewrite patterns.
Directory Structure
Each agent subdirectory has its own README with hook-specific details:
claude/— Shell hook,PreToolUseJSON format,settings.jsonpatching, test scriptcopilot/— Rust binary hook, dual format (VS Code Chat vs Copilot CLI), deny-with-suggestion fallbackcursor/— Shell hook, Cursor JSON format, empty{}response requirementcline/— Rules file (prompt-level),.clinerulesproject-local installationwindsurf/— Rules file (prompt-level),.windsurfrulesworkspace-scopedcodex/— Rust binary hook (rtk hook codex),PreToolUse.updatedInput,.codex/hooks.json/$CODEX_HOME/hooks.json, plusAGENTS.mdawarenessopencode/— TypeScript plugin,zxlibrary,tool.execute.beforeevent, in-place mutationpi/— TypeScript extension,tool_callevent, localisBashToolCallEventguard, in-place mutation,~/.pi/agent/extensions/; shared with Oh My Pi (OMP) — OMP installs the same file at.omp/extensions/via itslegacy-pi-compatlayerhermes/— Python plugin,pre_tool_callhook, in-place terminal command mutationvibe/— Rust binary hook (rtk hook vibe),pre_toolentry in~/.vibe/hooks.toml,hook_specific_output.tool_inputrewrite plussystem_messagefor UI visibility
Supported Agents
| Agent | Mechanism | Hook Type | Can Modify Command? |
|---|---|---|---|
| Claude Code | Shell hook (PreToolUse) |
Transparent rewrite | Yes (updatedInput) |
| VS Code Copilot Chat | Rust binary (rtk hook copilot) |
Transparent rewrite | Yes (updatedInput) |
| GitHub Copilot CLI | Rust binary (rtk hook copilot) |
Deny-with-suggestion | No (agent retries) |
| Cursor | Rust binary | Transparent rewrite | Yes (updated_input) |
| Trae | Rust binary (rtk hook trae) |
Transparent rewrite | Yes (updatedInput) |
| Gemini CLI | Rust binary (rtk hook gemini) |
Transparent rewrite | Yes (hookSpecificOutput) |
| Cline / Roo Code | Custom instructions (rules file) | Prompt-level guidance | N/A |
| Windsurf | Custom instructions (rules file) | Prompt-level guidance | N/A |
| Codex CLI | Rust binary (rtk hook codex) |
Transparent rewrite | Yes (updatedInput) |
| OpenCode | TypeScript plugin (tool.execute.before) |
In-place mutation | Yes |
| Pi | TypeScript extension (tool_call event) |
In-place mutation | Yes |
| Oh My Pi (OMP) | TypeScript extension (tool_call event, shared with Pi) |
In-place mutation | Yes |
| Hermes | Python plugin (pre_tool_call) |
In-place mutation | Yes |
| Mistral Vibe | Rust binary (rtk hook vibe) |
Transparent rewrite | Yes (hook_specific_output.tool_input) |
JSON Formats by Agent
Claude Code (Shell Hook)
Input (stdin):
{
"tool_name": "Bash",
"tool_input": { "command": "git status" }
}
Output (stdout, when rewritten):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": { "command": "rtk git status" }
}
}
Cursor (Shell Hook)
Input: Same as Claude Code.
Output (stdout, when rewritten):
{
"permission": "allow",
"updated_input": { "command": "rtk git status" }
}
Returns {} when no rewrite (Cursor requires JSON for all paths).
Trae (Rust Binary)
Install with rtk init --agent trae for .trae/hooks.json, or rtk init -g --agent trae for ~/.trae/hooks.json; global installation also updates ~/.trae-cn/hooks.json when that directory already exists.
Input: Trae PreToolUse payloads for RunCommand, such as:
{
"tool_name": "RunCommand",
"tool_input": { "command": "git status", "description": "Check status" }
}
Output (only when rewritten):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": { "command": "rtk git status", "description": "Check status" }
}
}
RTK deliberately does not set permissionDecision; Trae retains its own command-approval policy.
Codex CLI (Rust Binary)
Input (stdin):
{
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"permission_mode": "default",
"tool_input": { "command": "git status" }
}
Output (stdout, when rewritten):
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": { "command": "rtk git status" }
}
}
The allow value is required by Codex to accept updatedInput; Codex still runs its native approval and sandbox checks after applying the replacement. Those checks classify the rewritten command, and Codex does not currently unwrap the rtk binary for its safe/dangerous-command heuristics. Missing or unknown permission modes and other no-rewrite cases produce no stdout.
Copilot CLI (Rust Binary)
Input (stdin, camelCase, toolArgs is JSON-stringified):
{
"toolName": "bash",
"toolArgs": "{\"command\": \"git status\"}"
}
Output (no updatedInput support -- uses deny-with-suggestion):
{
"permissionDecision": "deny",
"permissionDecisionReason": "Token savings: use `rtk git status` instead"
}
VS Code Copilot Chat (Rust Binary)
Input (stdin, snake_case):
{
"tool_name": "Bash",
"tool_input": { "command": "git status" }
}
Output: Same as Claude Code format (with updatedInput).
Gemini CLI (Rust Binary)
Input (stdin):
{
"tool_name": "run_shell_command",
"tool_input": { "command": "git status" }
}
Output (when rewritten):
{
"decision": "allow",
"hookSpecificOutput": {
"tool_input": { "command": "rtk git status" }
}
}
No rewrite: {"decision": "allow"}
Mistral Vibe (Rust Binary)
Input (stdin):
{
"tool_name": "bash",
"tool_input": { "command": "git status" },
"hook_event_name": "pre_tool",
"session_id": "..."
}
Output (when rewritten):
{
"hook_specific_output": {
"tool_input": { "command": "rtk git status" }
},
"system_message": "rtk: rewrote to `rtk git status`"
}
No rewrite: exit 0 with empty stdout (Vibe's contract for "no opinion" from a pre_tool hook).
OpenCode (TypeScript Plugin)
Mutates args.command in-place via the zx library:
const result = await $`rtk rewrite ${command}`.quiet().nothrow()
const rewritten = String(result.stdout).trim()
if (rewritten && rewritten !== command) {
(args as Record<string, unknown>).command = rewritten
}
Hermes (Python Plugin)
Mutates args["command"] in-place via the pre_tool_call hook:
result = subprocess.run(["rtk", "rewrite", command], capture_output=True, text=True, timeout=2)
rewritten = result.stdout.strip()
if result.returncode in {0, 3} and rewritten and rewritten != command:
args["command"] = rewritten
Command Rewrite Registry
The registry (src/discover/registry.rs) handles command patterns across these categories:
| Category | Examples | Savings |
|---|---|---|
| Test Runners | vitest, pytest, cargo test, go test, playwright | 90-99% |
| Build Tools | cargo build, npm, pnpm, dotnet, make | 70-90% |
| VCS | git status/log/diff/show | 70-80% |
| Language Servers | tsc, mypy | 80-83% |
| Linters | eslint, ruff, golangci-lint, biome | 80-85% |
| Package Managers | pip, cargo install, pnpm list | 75-80% |
| File Operations | ls, find, grep, cat, head, tail | 60-75% |
| Infrastructure | docker, kubectl, aws, terraform | 75-85% |
Compound Command Handling
The registry handles &&, ||, ;, |, |&, and & operators:
- Pipe (
|): Producers and intermediate stages stay raw; only a pipeline-safe final stage is rewritten - Stderr pipe (
|&): The complete pipeline stays raw - And/Or/Semicolon (
&&,||,;): Both sides rewritten independently - Pipeline-safe rules: Initially limited to argument-safe
grep,rg, andwcinvocations; search pattern-file forms defer
Example: cargo fmt --all && cargo test becomes rtk cargo fmt --all && rtk cargo test
Override Controls
RTK_DISABLED=1: Per-command override (RTK_DISABLED=1 git statusruns raw)exclude_commands: In~/.config/rtk/config.toml, list commands to never rewrite. Matches against the full command after stripping env prefixes. Subcommand patterns work ("git push"excludesgit push origin main). Patterns starting with^are treated as regex.- Already-RTK:
rtk git statuspasses through unchanged (nortk rtk git)
Exit Code Contract
Hooks must never block command execution. All error paths (missing binary, bad JSON, rewrite failure) must exit 0 so the agent's command runs unmodified. A hook that exits non-zero prevents the user's command from executing.
When there is no rewrite to apply, the hook must produce no output (or {} for Cursor, which requires JSON on all paths).
Gaps (to be fixed)
hook_cmd.rs::run_gemini()— exits 1 on invalid JSON input instead of exit 0
Graceful Degradation
Hooks are non-blocking -- they never prevent a command from executing:
- jq not installed: warning to stderr, exit 0 (command runs raw)
- rtk binary not found: warning to stderr, exit 0
- rtk version too old (< 0.23.0): warning to stderr, exit 0
- Invalid JSON input: pass through unchanged
rtk rewritecrashes: hook exits 0 (subprocess error ignored)- Filter logic error: fallback to raw command output
Adding a New Agent Integration
New integrations must follow the Exit Code Contract and Graceful Degradation above, as well as the project's Design Philosophy.
Integration Tiers
| Tier | Mechanism | Maintenance | Examples |
|---|---|---|---|
| Full hook | Shell script or Rust binary, intercepts commands via agent's hook API | High — must track agent API changes | Claude Code, Cursor, Copilot, Gemini, Codex |
| Plugin | TypeScript/JS/Python plugin in agent's plugin system | Medium — agent manages loading | OpenCode, Hermes, Pi, Oh My Pi |
| Rules file | Prompt-level instructions the agent reads | Low — no code to break | Cline, Windsurf |
Eligibility
RTK supports AI coding assistants that developers actually use day-to-day. To add a new agent:
- Agent has a documented, stable hook/plugin API (not experimental/alpha)
- Agent is actively maintained (commit activity in last 3 months)
- Integration follows the exit code contract (exit 0 on all error paths)
- Hook output matches the agent's expected JSON format exactly
Maintenance
If an agent's API changes and the hook breaks, the integration should be updated promptly. If the agent becomes unmaintained or the hook can't be fixed, the integration may be deprecated with a release note.