How RTK Implements the Auto-Rewrite Hook for Claude Code

RTK intercepts shell commands in Claude Code by installing a PreToolUse hook that delegates rewriting to the rtk rewrite binary, then returns JSON responses via stdout that either auto-allow rewritten commands or trigger permission prompts based on structured exit codes.

The RTK auto-rewrite hook for Claude Code provides a security and efficiency layer by intercepting Bash tool calls before they reach the operating system. Found in the rtk-ai/rtk repository, this mechanism automatically converts raw shell commands into audited RTK equivalents, reducing token usage while maintaining strict permission boundaries. The system coordinates between a Bash delegate script and Rust core logic to process every command through Claude Code's native hook protocol.

The PreToolUse Hook Architecture

RTK installs a PreToolUse hook that sits between Claude Code’s Bash tool and the operating system. When Claude Code prepares to execute a shell command, it streams a JSON payload to the hook via stdin. The hook implementation in hooks/claude/rtk-rewrite.sh parses two possible formats: the VS Code-style payload used by Claude Code and the Copilot-CLI payload.

The hook extracts the raw command from the tool_input.command field, then delegates the rewrite decision to the RTK binary through the rtk rewrite "$CMD" sub-command. This architectural choice keeps the Bash script as a thin delegator while concentrating all rewrite logic in the Rust codebase, specifically within src/discover/registry.rs.

The Rust Rewrite Engine

The core rewrite logic resides in src/discover/registry.rs, implemented in the rewrite_command function. This function handles command trimming, exclusion pattern compilation, and segmentation of compound commands.

/// Returns the rewritten command or None if unsupported.
pub fn rewrite_command(cmd: &str, excluded: &[String]) -> Option<String> {
    let trimmed = cmd.trim();
    if trimmed.is_empty() || has_heredoc(trimmed) || trimmed.contains("$((") {
        return None;
    }

    let compiled = compile_exclude_patterns(excluded);

    // Already an RTK command → no change
    if !trimmed.contains("&&") && !trimmed.contains("||") &&
       !trimmed.contains(';') && !trimmed.contains('|') &&
       (trimmed.starts_with("rtk ") || trimmed == "rtk") {
        return Some(trimmed.to_string());
    }

    // Handle compound commands segment‑by‑segment.
    rewrite_compound(trimmed, &compiled)
}

The function first sanitizes input by discarding unsupported constructs like heredocs and arithmetic expansion $((…)). For compound commands containing &&, ||, ;, or |, it tokenizes the string and processes each segment independently via rewrite_compound. The function returns Some("<rtk-equivalent>") when a known RTK filter exists, otherwise None to indicate no rewrite is available.

Permission Handling and Exit Codes

Before rewriting occurs, the system checks permissions via src/hooks/permissions.rs. The PermissionVerdict enum determines whether to auto-allow, deny, or ask the user. If the verdict is Deny, the hook short-circuits the rewrite path and allows Claude Code's native deny handling to take over.

The hook interprets exit codes from the rtk rewrite command to construct the appropriate JSON response:

  • Exit code 0: Rewrite found with no deny/ask rule matched — triggers auto-allow with permissionDecision: "allow".
  • Exit code 1: No RTK equivalent exists — the original command passes through unchanged.
  • Exit code 2: A deny rule matched — defer to Claude Code's native deny mechanism.
  • Exit code 3: An ask rule matched — rewrite the command but omit permissionDecision to force a user prompt.

The JSON response structure follows Claude Code's hook protocol exactly. For auto-allow scenarios, the hook outputs:

{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "permissionDecisionReason": "RTK auto-rewrite",
    "updatedInput": { "command": "rtk git status" }
  }
}

When an ask rule triggers, the response omits the permissionDecision field to ensure Claude Code prompts the user before execution.

Hook Response Generation

The src/hooks/hook_cmd.rs file contains the handle_vscode function that orchestrates the response generation. This function performs permission checks first, then computes the rewritten command, and finally emits the JSON structure to stdout using writeln! to prevent protocol corruption.

fn handle_vscode(cmd: &str) -> Result<()> {
    // Permission check first
    let verdict = permissions::check_command(cmd);
    if verdict == PermissionVerdict::Deny {
        audit_log("deny", cmd, "");
        return Ok(());
    }

    // Compute rewritten command
    let rewritten = match get_rewritten(cmd) {
        Some(r) => r,
        None => return Ok(()),
    };

    // Decide whether Claude Code should be auto‑allowed or asked.
    let decision = match verdict {
        PermissionVerdict::Allow => "allow",
        _ => "ask",
    };

    audit_log("rewrite", cmd, &rewritten);

    let output = json!({
        "hookSpecificOutput": {
            "hookEventName": PRE_TOOL_USE_KEY,
            "permissionDecision": decision,
            "permissionDecisionReason": "RTK auto-rewrite",
            "updatedInput": { "command": rewritten }
        }
    });
    writeln!(io::stdout(), "{}", output)?;
    Ok(())
}

The Bash Hook Interface

The hooks/claude/rtk-rewrite.sh script serves as the entry point that Claude Code invokes. It reads the JSON payload from stdin, extracts the command using jq, and delegates to the Rust binary. Based on the exit code, it constructs the appropriate response using jq to ensure valid JSON formatting.

#!/usr/bin/env bash

INPUT=$(cat)                                   # Read JSON from Claude Code

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

# Delegate to the Rust binary

REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null)
EXIT_CODE=$?

case $EXIT_CODE in
  0)  # auto‑allow

      jq -c --arg cmd "$REWRITTEN" \
        '.tool_input.command = $cmd |
         {hookSpecificOutput:{hookEventName:"PreToolUse",
                              permissionDecision:"allow",
                              permissionDecisionReason:"RTK auto-rewrite",
                              updatedInput:.tool_input}}' <<<"$INPUT"
      ;;
  3)  # ask – omit permissionDecision

      jq -c --arg cmd "$REWRITTEN" \
        '.tool_input.command = $cmd |
         {hookSpecificOutput:{hookEventName:"PreToolUse",
                              updatedInput:.tool_input}}' <<<"$INPUT"
      ;;
  *)  # 1 (no rewrite) or 2 (deny) – pass through unchanged

      exit 0
      ;;
esac

Hook Installation and Registration

Registration occurs during rtk init -g, which writes a hook manifest to ~/.claude/hooks/rtk-rewrite.json. This manifest instructs Claude Code to invoke the rewrite script for every Bash tool call. The repository maintains a reference manifest at .github/hooks/rtk-rewrite.json that defines the hook configuration and entry point.

The complete data flow follows this path: Claude Code emits JSON to rtk-rewrite.sh, which calls rtk rewrite, triggering src/discover/registry.rs for logic and src/hooks/permissions.rs for policy decisions, with src/hooks/hook_cmd.rs handling the final JSON response construction back to Claude Code.

Summary

  • RTK installs a PreToolUse hook that intercepts Bash commands via JSON payloads on stdin before they reach the shell.
  • The Bash delegate (hooks/claude/rtk-rewrite.sh) extracts commands and invokes rtk rewrite, using exit codes to determine the response type.
  • Exit code 0 triggers auto-allow with rewritten commands, while exit code 3 triggers an ask decision that omits the permission field to force user confirmation.
  • The Rust rewrite engine in src/discover/registry.rs handles command parsing, exclusion filtering, and compound command segmentation via rewrite_compound.
  • Permission logic in src/hooks/permissions.rs evaluates rules before rewriting, supporting allow, deny, and ask verdicts that control the final JSON response structure.
  • Hook registration via rtk init -g places a manifest in the Claude Code hooks directory, enabling automatic interception for all subsequent Bash tool uses.

Frequently Asked Questions

What triggers the RTK auto-rewrite hook in Claude Code?

The hook triggers on every Bash tool invocation after running rtk init -g, which registers the manifest with Claude Code. The system intercepts any shell command, checks it against the RTK registry in src/discover/registry.rs, and automatically rewrites recognized commands to their rtk equivalents while leaving unknown commands unchanged.

How does RTK handle complex shell commands with pipes or semicolons?

RTK tokenizes compound commands containing &&, ||, ;, or | and processes each segment independently through the rewrite_compound function in src/discover/registry.rs. This allows the system to rewrite individual components of a command chain while preserving the original shell logic structure.

What is the difference between exit code 0 and exit code 3 in the RTK hook?

Exit code 0 indicates a successful rewrite with no restrictive permission rules matched, resulting in a JSON response containing "permissionDecision": "allow" that auto-executes the command. Exit code 3 indicates the command was rewritten but matches an "ask" rule, so the response omits the permissionDecision field, forcing Claude Code to prompt the user for confirmation before execution.

Where does Claude Code look for the RTK hook manifest?

Claude Code reads hook manifests from the ~/.claude/hooks/ directory. During installation, rtk init -g copies the manifest from .github/hooks/rtk-rewrite.json to that location, registering the rtk-rewrite.sh script as a PreToolUse hook for all Bash tool invocations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →