Deployed hook artifacts — the actual files installed on user machines by rtk init. These are shell scripts, TypeScript plugins, and rules files that run outside the Rust binary. They are thin delegates: parse agent-specific JSON, call rtk rewrite as a subprocess, format agent-specific response. Zero filtering logic lives here.
Owns: per-agent hook scripts and configuration files for 7 supported agents (Claude Code, Copilot, Cursor, Cline, Windsurf, Codex, OpenCode).
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.
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), delivering 60-90% token savings without requiring the agent or user to change their workflow.
Agent runs command (e.g., "cargo test --nocapture")
-> Hook intercepts (PreToolUse / plugin event)
-> Reads JSON input, extracts command string
-> Calls `rtk rewrite "cargo test --nocapture"`
-> 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 (~90% fewer tokens)
All rewrite logic lives in the Rust binary (src/discover/registry.rs). Hook scripts are thin delegates that handle agent-specific JSON formats and call rtk rewrite for the actual decision. This ensures a single source of truth for all 70+ rewrite patterns.
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/— Awareness document,AGENTS.mdintegration,$CODEX_HOMEor~/.codex/locationopencode/— TypeScript plugin,zxlibrary,tool.execute.beforeevent, in-place mutation
| 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 | Shell hook (preToolUse) |
Transparent rewrite | Yes (updated_input) |
| 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 | AGENTS.md / instructions | Prompt-level guidance | N/A |
| OpenCode | TypeScript plugin (tool.execute.before) |
In-place mutation | Yes |
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" }
}
}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).
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"
}Input (stdin, snake_case):
{
"tool_name": "Bash",
"tool_input": { "command": "git status" }
}Output: Same as Claude Code format (with updatedInput).
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"}
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
}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% |
The registry handles &&, ||, ;, |, and & operators:
- Pipe (
|): Only the left side is rewritten (right side consumes output format) - And/Or/Semicolon (
&&,||,;): Both sides rewritten independently - find/fd in pipes: Never rewritten (output format incompatible with xargs/wc/grep)
Example: cargo fmt --all && cargo test becomes rtk cargo fmt --all && rtk cargo test
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)
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).
hook_cmd.rs::run_gemini()— exits 1 on invalid JSON input instead of exit 0
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
New integrations must follow the Exit Code Contract and Graceful Degradation above, as well as the project's Design Philosophy.
| 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 |
| Plugin | TypeScript/JS plugin in agent's plugin system | Medium — agent manages loading | OpenCode |
| Rules file | Prompt-level instructions the agent reads | Low — no code to break | Cline, Windsurf, Codex |
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
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.