Skip to main content

What Are Hooks?

Claude Code hooks are scripts that intercept tool execution at specific lifecycle points. RTK uses the PreToolUse hook to modify Bash commands before they reach the shell.
Hook lifecycle: Claude decides to run a command → PreToolUse hook intercepts → Hook rewrites command → Modified command executes → Claude receives output

PreToolUse Hook Workflow

Claude Code types:  git status


┌────────────────────────────────┐
│  ~/.claude/settings.json        │
│  PreToolUse hook registered     │
└────────────────────────────────┘


┌────────────────────────────────┐
│  rtk-rewrite.sh                 │
│  Input: "git status"            │
│  Output: "rtk git status"       │  ← Transparent rewrite
└────────────────────────────────┘


┌────────────────────────────────┐
│  RTK (Rust binary)              │
│  Executes: git status           │
│  Filters output                 │
└────────────────────────────────┘


Claude receives:  "3 modified, 1 untracked ✓"
                  ↑ not 50 lines of raw git output
The rewrite is transparent: Claude never knows the command was modified. It just sees optimized output.

Auto-Rewrite Hook Architecture

RTK’s hook script (rtk-rewrite.sh) is a Bash script that:
  1. Receives: JSON input from Claude Code with the original command
  2. Parses: Extracts the command string
  3. Matches: Pattern-matches against known RTK commands
  4. Rewrites: Transforms to rtk <cmd> equivalent
  5. Returns: JSON output with updatedInput field
If no match is found, the hook returns empty JSON (command passes through unchanged).

Hook Script Structure

#!/bin/bash
# Guards: check dependencies before set -euo pipefail
if ! command -v rtk &>/dev/null || ! command -v jq &>/dev/null; then
  exit 0  # Silently skip if dependencies missing
fi

set -euo pipefail

INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')

# Skip if empty or already using rtk
if [ -z "$CMD" ] || [[ "$CMD" =~ ^rtk\ ]]; then
  exit 0
fi

# Pattern matching and rewriting...
REWRITTEN=""

if echo "$CMD" | grep -qE '^git[[:space:]]+status'; then
  REWRITTEN=$(echo "$CMD" | sed 's/^git status/rtk git status/')
elif echo "$CMD" | grep -qE '^cargo[[:space:]]+test'; then
  REWRITTEN=$(echo "$CMD" | sed 's/^cargo test/rtk cargo test/')
# ... more patterns
fi

# Return JSON with updated command
if [ -n "$REWRITTEN" ]; then
  jq -n --arg cmd "$REWRITTEN" '{
    "hookSpecificOutput": {
      "hookEventName": "PreToolUse",
      "permissionDecision": "allow",
      "updatedInput": {"command": $cmd}
    }
  }'
fi

Guards and Safety

The hook includes guard clauses to handle edge cases:
  • Dependency check: Exits silently if rtk or jq not found
  • Empty command: Skips if no command string
  • Already rewritten: Skips if command already starts with rtk
  • Heredocs: Skips commands with << (shell syntax incompatible with rewriting)
Guards must appear before set -euo pipefail. Otherwise, missing dependencies cause the hook to crash instead of failing gracefully.

How the Hook Works

1. Command Detection

The hook pattern-matches the first word of the command:
# Matches:
git status
git log -10
cargo test --release

# Does NOT match (already using rtk):
rtk git status
rtk cargo test

2. Environment Variable Preservation

Commands with environment variable prefixes are handled correctly:
# Input:
TEST_SESSION_ID=2 npx playwright test

# Output:
TEST_SESSION_ID=2 rtk playwright test
The hook strips env vars for pattern matching, then re-adds them after rewriting.

3. Command Chaining

The hook only rewrites the first command in chains:
# Input:
git add . && git commit -m "fix" && git push

# Output:
rtk git add . && rtk git commit -m "fix" && rtk git push
Each command in the chain is rewritten independently because Claude Code invokes the hook once per command.

4. Passthrough Commands

Commands without RTK support pass through unchanged:
# Input:
echo "hello"

# Output:
echo "hello"  # No rewrite
RTK’s passthrough support means unhandled commands still work.

Commands Rewritten

The hook rewrites the following commands:

Version Control

Raw CommandRewritten To
git status/diff/log/add/commit/push/pull/branch/fetch/stash/showrtk git ...
gh pr/issue/run/api/releasertk gh ...

Build & Test

Raw CommandRewritten To
cargo test/build/clippy/check/install/nextest/fmtrtk cargo ...
go test/build/vetrtk go ...
golangci-lintrtk golangci-lint

JavaScript/TypeScript

Raw CommandRewritten To
vitest / pnpm testrtk vitest run
tsc / vue-tscrtk tsc
eslint / pnpm lintrtk lint
prettierrtk prettier
playwrightrtk playwright
prismartk prisma
npm test / npm run <script>rtk npm ...
pnpm list/ls/outdatedrtk pnpm ...

Python

Raw CommandRewritten To
pytest / python -m pytestrtk pytest
ruff check/formatrtk ruff ...
pip list/outdated/install/showrtk pip ...
uv pip ...rtk pip ...
mypy / python -m mypyrtk mypy
Raw CommandRewritten To
cat <file>rtk read <file>
rg/grep <pattern>rtk grep <pattern>
lsrtk ls
treertk tree
find <pattern>rtk find <pattern>
diffrtk diff
head -N <file>rtk read <file> --max-lines N

Containers

Raw CommandRewritten To
docker ps/images/logs/run/build/exec/composertk docker ...
kubectl get/logs/describe/applyrtk kubectl ...

Network

Raw CommandRewritten To
curlrtk curl
wgetrtk wget

Infrastructure

Raw CommandRewritten To
awsrtk aws
psqlrtk psql
Commands already using rtk, heredocs (<<), and unrecognized commands pass through unchanged.

Installation

The hook installs automatically with:
rtk init --global
This creates:
  • ~/.claude/hooks/rtk-rewrite.sh (executable, with guards)
  • Entry in ~/.claude/settings.json
See Claude Code Integration for full walkthrough.

Manual Installation

If automatic setup fails:
1

Create hook directory

mkdir -p ~/.claude/hooks
2

Copy hook script

# From RTK source repo:
cp .claude/hooks/rtk-rewrite.sh ~/.claude/hooks/rtk-rewrite.sh
chmod +x ~/.claude/hooks/rtk-rewrite.sh
3

Edit settings.json

Add to ~/.claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/rtk-rewrite.sh"
          }
        ]
      }
    ]
  }
}
4

Restart Claude Code

Close and reopen Claude Code, then test:
git status  # Should show compact output

Per-Project Installation

For project-specific hooks, create .claude/hooks/rtk-rewrite.sh in your project root and reference it in .claude/settings.json:
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "./.claude/hooks/rtk-rewrite.sh"
          }
        ]
      }
    ]
  }
}

Hook Debugging

Enable Audit Logging

Track every hook invocation:
export RTK_HOOK_AUDIT=1
Logs are written to ~/.local/share/rtk/hook-audit.log:
2026-03-05T14:23:45Z | rewrite | git status | rtk git status
2026-03-05T14:24:10Z | skip:already_rtk | rtk cargo test | -
2026-03-05T14:24:35Z | skip:no_match | echo hello | -
Columns:
  1. Timestamp (UTC)
  2. Action (rewrite, skip:*)
  3. Original command
  4. Rewritten command (or - if skipped)

Custom Audit Directory

export RTK_AUDIT_DIR="/tmp/rtk-audit"

Verify Hook Execution

Check if the hook is being invoked:
# In Claude Code conversation:
git status

# Then check audit log:
tail -1 ~/.local/share/rtk/hook-audit.log
Expected:
2026-03-05T14:30:00Z | rewrite | git status | rtk git status

Verify Hook Permissions

ls -l ~/.claude/hooks/rtk-rewrite.sh
Should show -rwxr-xr-x (executable). If not executable:
chmod +x ~/.claude/hooks/rtk-rewrite.sh

Test Hook Manually

Run the hook script directly:
echo '{"tool_input": {"command": "git status"}}' | ~/.claude/hooks/rtk-rewrite.sh
Expected output:
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "command": "rtk git status"
    }
  }
}

Alternative: Suggest Hook

If you prefer Claude Code to suggest rtk usage rather than automatically rewriting, use the suggest hook pattern.

Auto-Rewrite vs Suggest

AspectAuto-Rewrite HookSuggest Hook
StrategyModifies command before executionEmits system reminder when rtk-compatible command detected
EffectClaude never sees original commandClaude receives hint to use rtk
Adoption100% (forced)~70-85% (depends on adherence)
Use CaseProduction, guaranteed savingsLearning mode, auditing
OverheadZero (transparent)Minimal (reminder in context)

When to Use Suggest

  • You want to audit which commands Claude chooses to run
  • You’re learning rtk patterns and want visibility
  • You prefer explicit decisions over transparent rewrites
  • You want to preserve exact command execution for debugging

Suggest Hook Setup

1

Create suggest hook

mkdir -p ~/.claude/hooks
cp .claude/hooks/rtk-suggest.sh ~/.claude/hooks/rtk-suggest.sh
chmod +x ~/.claude/hooks/rtk-suggest.sh
2

Register in settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/rtk-suggest.sh"
          }
        ]
      }
    ]
  }
}
3

Restart Claude Code

Test with any rtk-compatible command. Claude will receive a system reminder but execute the original command.
The suggest hook outputs systemMessage instead of updatedInput, informing Claude that an rtk alternative exists without modifying the command.

Troubleshooting

Hook Not Rewriting Commands

Symptom: Commands show raw output after hook installation. Checks:
  1. Verify hook is registered:
    cat ~/.claude/settings.json | grep rtk-rewrite
    
  2. Verify hook is executable:
    test -x ~/.claude/hooks/rtk-rewrite.sh && echo "Executable" || echo "Not executable"
    
  3. Check dependencies:
    command -v rtk && command -v jq
    
  4. Enable audit logging:
    export RTK_HOOK_AUDIT=1
    
    Then run a command and check logs.

Hook Crashes

Symptom: Claude Code shows “Hook execution failed” error. Common causes:
  • set -euo pipefail runs before guards → Move guards to top of script
  • Missing jq dependency → Install: brew install jq / apt install jq
  • Invalid JSON output → Test hook manually with echo '{...}' | hook.sh

Hook Ignores Certain Commands

Symptom: Some commands rewrite, others don’t. Cause: Pattern doesn’t match command syntax. Example: npm run test vs npm test Check the hook script patterns at ~/.claude/hooks/rtk-rewrite.sh around line 130-140 for npm handling.

Next Steps

Configuration

Customize RTK behavior and settings

Claude Code Setup

Full installation walkthrough