# How RTK Implements the Auto-Rewrite Hook for Claude Code

> Learn how RTK implements the auto-rewrite hook for Claude Code to intercept shell commands. Discover how RTK enables secure command execution through JSON responses and structured exit codes.

- Repository: [rtk-ai/rtk](https://github.com/rtk-ai/rtk)
- Tags: internals
- Published: 2026-04-24

---

**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](https://github.com/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`](https://github.com/rtk-ai/rtk/blob/main/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`](https://github.com/rtk-ai/rtk/blob/main/src/discover/registry.rs).

## The Rust Rewrite Engine

The core rewrite logic resides in [`src/discover/registry.rs`](https://github.com/rtk-ai/rtk/blob/main/src/discover/registry.rs), implemented in the `rewrite_command` function. This function handles command trimming, exclusion pattern compilation, and segmentation of compound commands.

```rust
/// 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`](https://github.com/rtk-ai/rtk/blob/main/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:

```json
{
  "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`](https://github.com/rtk-ai/rtk/blob/main/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.

```rust
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`](https://github.com/rtk-ai/rtk/blob/main/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.

```bash
#!/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`](https://github.com/rtk-ai/rtk/blob/main/.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`](https://github.com/rtk-ai/rtk/blob/main/rtk-rewrite.sh), which calls `rtk rewrite`, triggering [`src/discover/registry.rs`](https://github.com/rtk-ai/rtk/blob/main/src/discover/registry.rs) for logic and [`src/hooks/permissions.rs`](https://github.com/rtk-ai/rtk/blob/main/src/hooks/permissions.rs) for policy decisions, with [`src/hooks/hook_cmd.rs`](https://github.com/rtk-ai/rtk/blob/main/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`](https://github.com/rtk-ai/rtk/blob/main/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`](https://github.com/rtk-ai/rtk/blob/main/src/discover/registry.rs) handles command parsing, exclusion filtering, and compound command segmentation via `rewrite_compound`.
- **Permission logic** in [`src/hooks/permissions.rs`](https://github.com/rtk-ai/rtk/blob/main/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`](https://github.com/rtk-ai/rtk/blob/main/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`](https://github.com/rtk-ai/rtk/blob/main/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`](https://github.com/rtk-ai/rtk/blob/main/.github/hooks/rtk-rewrite.json) to that location, registering the [`rtk-rewrite.sh`](https://github.com/rtk-ai/rtk/blob/main/rtk-rewrite.sh) script as a PreToolUse hook for all Bash tool invocations.