Back to Rtk

LLM Agent Hooks

hooks/README.md

0.38.09.3 KB
Original Source

LLM Agent Hooks

Scope

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.

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), delivering 60-90% token savings 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
  -> 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.

Directory Structure

Each agent subdirectory has its own README with hook-specific details:

  • claude/ — Shell hook, PreToolUse JSON format, settings.json patching, test script
  • copilot/ — Rust binary hook, dual format (VS Code Chat vs Copilot CLI), deny-with-suggestion fallback
  • cursor/ — Shell hook, Cursor JSON format, empty {} response requirement
  • cline/ — Rules file (prompt-level), .clinerules project-local installation
  • windsurf/ — Rules file (prompt-level), .windsurfrules workspace-scoped
  • codex/ — Awareness document, AGENTS.md integration, $CODEX_HOME or ~/.codex/ location
  • opencode/ — TypeScript plugin, zx library, tool.execute.before event, in-place mutation

Supported Agents

AgentMechanismHook TypeCan Modify Command?
Claude CodeShell hook (PreToolUse)Transparent rewriteYes (updatedInput)
VS Code Copilot ChatRust binary (rtk hook copilot)Transparent rewriteYes (updatedInput)
GitHub Copilot CLIRust binary (rtk hook copilot)Deny-with-suggestionNo (agent retries)
CursorShell hook (preToolUse)Transparent rewriteYes (updated_input)
Gemini CLIRust binary (rtk hook gemini)Transparent rewriteYes (hookSpecificOutput)
Cline / Roo CodeCustom instructions (rules file)Prompt-level guidanceN/A
WindsurfCustom instructions (rules file)Prompt-level guidanceN/A
Codex CLIAGENTS.md / instructionsPrompt-level guidanceN/A
OpenCodeTypeScript plugin (tool.execute.before)In-place mutationYes

JSON Formats by Agent

Claude Code (Shell Hook)

Input (stdin):

json
{
  "tool_name": "Bash",
  "tool_input": { "command": "git status" }
}

Output (stdout, when rewritten):

json
{
  "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):

json
{
  "permission": "allow",
  "updated_input": { "command": "rtk git status" }
}

Returns {} when no rewrite (Cursor requires JSON for all paths).

Copilot CLI (Rust Binary)

Input (stdin, camelCase, toolArgs is JSON-stringified):

json
{
  "toolName": "bash",
  "toolArgs": "{\"command\": \"git status\"}"
}

Output (no updatedInput support -- uses deny-with-suggestion):

json
{
  "permissionDecision": "deny",
  "permissionDecisionReason": "Token savings: use `rtk git status` instead"
}

VS Code Copilot Chat (Rust Binary)

Input (stdin, snake_case):

json
{
  "tool_name": "Bash",
  "tool_input": { "command": "git status" }
}

Output: Same as Claude Code format (with updatedInput).

Gemini CLI (Rust Binary)

Input (stdin):

json
{
  "tool_name": "run_shell_command",
  "tool_input": { "command": "git status" }
}

Output (when rewritten):

json
{
  "decision": "allow",
  "hookSpecificOutput": {
    "tool_input": { "command": "rtk git status" }
  }
}

No rewrite: {"decision": "allow"}

OpenCode (TypeScript Plugin)

Mutates args.command in-place via the zx library:

typescript
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
}

Command Rewrite Registry

The registry (src/discover/registry.rs) handles command patterns across these categories:

CategoryExamplesSavings
Test Runnersvitest, pytest, cargo test, go test, playwright90-99%
Build Toolscargo build, npm, pnpm, dotnet, make70-90%
VCSgit status/log/diff/show70-80%
Language Serverstsc, mypy80-83%
Linterseslint, ruff, golangci-lint, biome80-85%
Package Managerspip, cargo install, pnpm list75-80%
File Operationsls, find, grep, cat, head, tail60-75%
Infrastructuredocker, kubectl, aws, terraform75-85%

Compound Command Handling

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

Override Controls

  • RTK_DISABLED=1: Per-command override (RTK_DISABLED=1 git status runs 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" excludes git push origin main). Patterns starting with ^ are treated as regex.
  • Already-RTK: rtk git status passes through unchanged (no rtk 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 rewrite crashes: 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

TierMechanismMaintenanceExamples
Full hookShell script or Rust binary, intercepts commands via agent's hook APIHigh — must track agent API changesClaude Code, Cursor, Copilot, Gemini
PluginTypeScript/JS plugin in agent's plugin systemMedium — agent manages loadingOpenCode
Rules filePrompt-level instructions the agent readsLow — no code to breakCline, Windsurf, Codex

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.