Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 22 additions & 25 deletions docs/contributing/TECHNICAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,11 @@ This is the full lifecycle of a command through RTK, from LLM agent to filtered

The user runs `rtk init` to set up hooks for their LLM agent. This:

1. Writes a thin shell hook script (e.g., `~/.claude/hooks/rtk-rewrite.sh`)
2. Stores its SHA-256 hash for integrity verification
3. Patches the agent's settings file (e.g., `settings.json`) to register the hook
4. Writes RTK awareness instructions (e.g., `RTK.md`) for prompt-level guidance
1. Patches the agent's settings file (e.g., `settings.json`) to register the hook
2. Registers the RTK binary directly when the agent supports it; other integrations install their required script or plugin
3. Writes RTK awareness instructions (e.g., `RTK.md`) for prompt-level guidance

RTK supports 7 agents, each with its own installation mode. The hook scripts are embedded in the binary and written at install time.
Claude Code uses the shell-free `rtk hook claude` binary entry. Other agents use the installation mode described in their hook documentation.

> **Details**: [`src/hooks/README.md`](../src/hooks/README.md) covers all installation modes, configuration files, and the uninstall flow.

Expand All @@ -86,11 +85,11 @@ RTK supports 7 agents, each with its own installation mode. The hook scripts are
When an LLM agent runs a command (e.g., `git status`):

1. The agent fires a `PreToolUse` event (or equivalent) containing the command as JSON
2. The hook script reads the JSON, extracts the command string
3. The hook calls `rtk rewrite "git status"` as a subprocess
4. `rtk rewrite` consults the command registry and returns `rtk git status`
2. The agent-specific hook reads the JSON and extracts the command string
3. The native hook or delegate calls RTK's rewrite logic
4. The command registry returns `rtk git status` when a rewrite applies
5. The hook sends a response telling the agent to use the rewritten command
6. If anything fails (jq missing, rtk not found, no match), the hook exits silently -- the raw command runs unchanged
6. If parsing or rewriting fails, or no rule matches, the hook exits silently -- the raw command runs unchanged

All rewrite logic lives in Rust (`src/discover/registry.rs`). Hooks are thin delegates that handle agent-specific JSON formats.

Expand All @@ -101,37 +100,35 @@ All rewrite logic lives in Rust (`src/discover/registry.rs`). Hooks are thin del
The rewrite pipeline is how RTK intercepts and rewrites commands. The call chain is:

```
hook shellrewrite_cmd.rs → rewrite_command() → rewrite_compound() → rewrite_segment() → classify_command()
rtk hook claudehook_cmd.rs → rewrite_command() → rewrite_compound() → rewrite_segment() → classify_command()
```

Traced step by step for `cargo fmt --all && cargo test 2>&1 | tail -20`:

```
LLM Agent: "cargo fmt --all && cargo test 2>&1 | tail -20"
|
| Hook shell (hooks/claude/rtk-rewrite.sh)
| Reads JSON from agent, extracts command, calls `rtk rewrite "$CMD"`
| On failure (jq missing, rtk missing, old version): exit 0 (passthrough)
| Native hook (`rtk hook claude`)
| Reads JSON from the agent
| On failure: exit 0 (passthrough)
|
v
rewrite_cmd::run(cmd) [src/hooks/rewrite_cmd.rs]
| 1. Load config → hooks.exclude_commands
| 2. check_command(cmd) → Deny → exit(2)
| 3. registry::rewrite_command(cmd, excluded)
| → None → exit(1) (no RTK equivalent, passthrough)
| → Some + Allow → print, exit(0)
| → Some + Ask → print, exit(3)
hook_cmd::run_claude() [src/hooks/hook_cmd.rs]
| 1. Parse the bounded stdin payload
| 2. Check hook permissions
| 3. Call registry::rewrite_command(cmd, excluded, transparent_prefixes)
| 4. Emit Claude Code's updatedInput JSON, or pass through silently
|
v
rewrite_command(cmd, excluded) [src/discover/registry.rs]
rewrite_command(...) [src/discover/registry.rs]
| Early exits:
| - Empty → None
| - Contains "<<" or "$((" (heredoc/arithmetic) → None
| - Simple "rtk ..." (no operators) → return as-is
| - Otherwise → rewrite_compound(cmd, excluded)
| - Otherwise → rewrite_compound(...)
|
v
rewrite_compound(cmd, excluded) [src/discover/registry.rs]
rewrite_compound(...) [src/discover/registry.rs]
|
| Step 1 — Tokenize (lexer.rs)
| tokenize() produces typed tokens with byte offsets:
Expand Down Expand Up @@ -313,7 +310,7 @@ Start here, then drill down into each README for file-level details.
| Directory | Agent | What you'll find in its README |
|-----------|-------|-------------------------------|
| [`hooks/`](../hooks/README.md) | _(parent)_ | **All JSON formats**, rewrite registry overview, exit code contract, override controls |
| [`claude/`](../hooks/claude/README.md) | Claude Code | Shell hook mechanism, `PreToolUse` JSON, test script |
| [`claude/`](../hooks/claude/README.md) | Claude Code | Awareness file, legacy shell hook, and compatibility tests |
| [`copilot/`](../hooks/copilot/README.md) | GitHub Copilot | Rust binary hook, single `PreToolUse` schema shared by VS Code Chat and Copilot CLI |
| [`cursor/`](../hooks/cursor/README.md) | Cursor IDE | Shell hook, empty JSON response requirement |
| [`cline/`](../hooks/cline/README.md) | Cline / Roo Code | Rules file (prompt-level, no programmatic hook) |
Expand All @@ -329,7 +326,7 @@ RTK supports the following LLM agents through hook integrations:

| Agent | Hook Type | Mechanism | Can Modify Command? |
|-------|-----------|-----------|---------------------|
| Claude Code | Shell hook | `PreToolUse` in `settings.json` | Yes (`updatedInput`) |
| Claude Code | Rust binary | `rtk hook claude` via `PreToolUse` | Yes (`updatedInput`) |
| GitHub Copilot (VS Code) | Rust binary | `rtk hook copilot` reads JSON | Yes (`updatedInput`) |
| GitHub Copilot CLI | Rust binary | `rtk hook copilot` reads JSON | Yes (`updatedInput`) |
| Cursor | Rust binary | `rtk hook cursor` reads JSON | Yes (`updated_input`) |
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/getting-started/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ Download from [GitHub releases](https://github.com/rtk-ai/rtk/releases):
- Linux: `rtk-x86_64-unknown-linux-musl.tar.gz` / `rtk-aarch64-unknown-linux-gnu.tar.gz`
- Windows: `rtk-x86_64-pc-windows-msvc.zip`

**Windows users**: Extract the zip and place `rtk.exe` in a directory on your PATH. Run RTK from Command Prompt, PowerShell, or Windows Terminal — do not double-click the `.exe` (it prints usage and exits immediately). For full hook support, use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) instead.
**Windows users**: Extract the zip and place `rtk.exe` in a directory on your PATH. Run RTK from Command Prompt, PowerShell, or Windows Terminal — do not double-click the `.exe` (it prints usage and exits immediately). Claude Code's native RTK hook works without WSL.

## Verify installation

Expand Down
16 changes: 9 additions & 7 deletions docs/guide/getting-started/supported-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Agent runs "cargo test"

| Agent | Integration tier | Can rewrite transparently? |
|-------|-----------------|---------------------------|
| Claude Code | Shell hook (`PreToolUse`) | Yes |
| Claude Code | Rust binary (`rtk hook claude`, `PreToolUse`) | Yes |
| VS Code Copilot Chat | Shell hook (`PreToolUse`) | Yes |
| GitHub Copilot CLI | Shell hook (`PreToolUse`) | Yes |
| Cursor | Shell hook (`preToolUse`) | Yes |
Expand Down Expand Up @@ -232,13 +232,15 @@ Rules file integrations (Cline, Windsurf, Codex, Kilo Code, Antigravity) rely on

## Windows support

The shell hook (`rtk-rewrite.sh`) requires a Unix shell. On native Windows:
Claude Code uses the native RTK binary on every platform. `rtk init -g`
registers `rtk` as the executable and passes `hook claude` as an argument array,
so native Windows does not need a Unix shell for this hook. Existing absolute
paths ending in `rtk.exe` are recognized as installed when they use the same
arguments.

- `rtk init -g` automatically falls back to **CLAUDE.md injection mode** (prompt-level instructions)
- Filters work normally (`rtk cargo test`, `rtk git status`)
- Auto-rewrite does not work — the AI assistant is instructed to use RTK but commands are not intercepted

For full hook support on Windows, use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install). Inside WSL, all agents with shell hook integration (Claude Code, Cursor, Gemini) work identically to Linux.
Legacy `rtk-rewrite.sh` integrations still require a Unix shell. Re-run
`rtk init -g` to migrate RTK's older combined Claude Code command entry to the
native form; use WSL only when another shell-script integration requires it.

## Graceful degradation

Expand Down
15 changes: 6 additions & 9 deletions docs/guide/resources/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,17 @@ rtk --version
- Or open PowerShell or Windows Terminal
- Then run: `rtk --version`

### Hook not working (no auto-rewrite)
### Claude Code hook reported as missing

**Symptom:** `rtk init -g` shows "Falling back to --claude-md mode" on Windows.
**Symptom:** The RTK hook is configured on native Windows, but `rtk init --show` or `rtk gain` reports it as missing.

**Cause:** The auto-rewrite hook (`rtk-rewrite.sh`) requires a Unix shell. Native Windows doesn't have one.
**Fix:** Update RTK and re-run `rtk init -g`. Current versions register the executable and its arguments separately, without a Unix shell:

**Fix:** Use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) for full hook support:
```bash
# Inside WSL
curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh
rtk init -g # full hook mode works in WSL
```json
{ "type": "command", "command": "rtk", "args": ["hook", "claude"] }
```

On native Windows, RTK falls back to CLAUDE.md injection. Your AI assistant gets RTK instructions but won't auto-rewrite commands. It can still use RTK manually: `rtk cargo test`, `rtk git status`, etc.
An absolute path ending in `rtk.exe` is also recognized with the same arguments. Keep `rtk.exe` on PATH when using the generated form, then restart Claude Code.

### Node.js tools not found

Expand Down
14 changes: 11 additions & 3 deletions hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ All rewrite logic lives in the Rust binary (`src/discover/registry.rs`). Hook sc

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

- **[`claude/`](claude/README.md)** — Shell hook, `PreToolUse` JSON format, `settings.json` patching, test script
- **[`claude/`](claude/README.md)** — Awareness file plus the legacy shell hook and its compatibility tests; current installs use `rtk hook claude`
- **[`copilot/`](copilot/README.md)** — Rust binary hook, dual format (VS Code Chat vs Copilot CLI), deny-with-suggestion fallback
- **[`cursor/`](cursor/README.md)** — Shell hook, Cursor JSON format, empty `{}` response requirement
- **[`cline/`](cline/README.md)** — Rules file (prompt-level), `.clinerules` project-local installation
Expand All @@ -48,7 +48,7 @@ Each agent subdirectory has its own README with hook-specific details:

| Agent | Mechanism | Hook Type | Can Modify Command? |
|-------|-----------|-----------|---------------------|
| Claude Code | Shell hook (`PreToolUse`) | Transparent rewrite | Yes (`updatedInput`) |
| Claude Code | Rust binary (`rtk hook claude`, `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 | Rust binary | Transparent rewrite | Yes (`updated_input`) |
Expand All @@ -63,7 +63,15 @@ Each agent subdirectory has its own README with hook-specific details:

## JSON Formats by Agent

### Claude Code (Shell Hook)
### Claude Code (Rust Binary Hook)

`rtk init -g` registers the hook as a shell-free executable plus argument array:

```json
{ "type": "command", "command": "rtk", "args": ["hook", "claude"] }
```

Legacy combined command strings remain detectable for compatibility.

**Input** (stdin):

Expand Down
20 changes: 14 additions & 6 deletions src/hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ LLM agent integration layer that installs, validates, and executes command-rewri

| Mode | Command | Creates | Patches |
|------|---------|---------|----------|
| Default (global) | `rtk init -g` | Hook, SHA-256 hash, RTK.md | settings.json, CLAUDE.md |
| Hook only | `rtk init -g --hook-only` | Hook, SHA-256 hash | settings.json |
| Default (global) | `rtk init -g` | RTK.md | settings.json, CLAUDE.md |
| Hook only | `rtk init -g --hook-only` | -- | settings.json |
| Claude-MD (legacy) | `rtk init --claude-md` | 134-line RTK block | CLAUDE.md |
| Windsurf | `rtk init -g --agent windsurf` | `.windsurfrules` | -- |
| Cline | `rtk init --agent cline` | `.clinerules` | -- |
Expand All @@ -34,15 +34,17 @@ LLM agent integration layer that installs, validates, and executes command-rewri
| Hermes | `rtk init --agent hermes` | Python plugin in `~/.hermes/plugins/rtk-rewrite/` | `config.yaml` `plugins.enabled` |


## Integrity Verification
## Legacy Shell-Hook Integrity Verification

The integrity system prevents unauthorized hook modifications:
Current Claude Code installs run the RTK binary directly, so they do not create
a hook script or hash file. The integrity system remains for older
`rtk-rewrite.sh` installations until they are migrated:

1. At install: `integrity::store_hash()` computes SHA-256 of the hook file, writes to `~/.claude/hooks/.rtk-hook.sha256` (read-only 0o444)
2. At runtime: `integrity::runtime_check()` re-computes hash and compares; blocks execution if tampered
3. On demand: `rtk verify` prints detailed verification status (PASS/FAIL/WARN/SKIP)

Five integrity states:
Five legacy integrity states:
- **Verified**: Hash matches stored value
- **Tampered**: Hash mismatch (blocks execution)
- **NoBaseline**: Hook exists but no hash stored (old install)
Expand All @@ -59,6 +61,12 @@ Controls how `rtk init` modifies agent settings files:
| Auto | `--auto-patch` | Patches without prompting; for CI/scripted installs |
| Skip | `--no-patch` | Prints manual instructions; user patches manually |

Claude Code is registered without a shell wrapper: `settings.json` stores
`"command": "rtk"` and `"args": ["hook", "claude"]`. Detection also accepts
absolute `rtk`/`rtk.exe` paths with the same arguments and the legacy combined
command string. When patching is allowed, `rtk init -g` upgrades the exact
legacy entry it previously generated while preserving fields such as `timeout`.

## Atomicity and Safety

All file operations use atomic writes (tempfile + rename) to prevent corruption on crash. Settings files are backed up to `.bak` before modification. All operations are idempotent -- running `rtk init` multiple times is safe.
Expand All @@ -84,7 +92,7 @@ Rules are loaded from all Claude Code `settings.json` files (project + global, i

| Tool | ask support | Behavior on Default |
|------|------------|-------------------|
| Claude Code (rtk-rewrite.sh) | Yes | `permissionDecision: "ask"` — user prompted |
| Claude Code (`rtk hook claude`) | Yes | `permissionDecision: "ask"` — user prompted |
| Copilot VS Code (rtk hook copilot) | Yes | `permissionDecision: "ask"` — user prompted |
| Cursor (rtk hook cursor) | Ready | `permission: "ask",` — users will be prompted when Cursor enforces the permission; in the meantime, allow |
| Gemini CLI (rtk hook gemini) | No (allow/deny only) | allow (limitation — no ask mode in Gemini) |
Expand Down
5 changes: 4 additions & 1 deletion src/hooks/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ pub const HOOKS_JSON: &str = "hooks.json";
pub const PRE_TOOL_USE_KEY: &str = "PreToolUse";
pub const BEFORE_TOOL_KEY: &str = "BeforeTool";

/// Native Rust hook command for Claude Code (replaces rtk-rewrite.sh).
/// Human-readable command and the exact shell form emitted by older RTK versions.
pub const CLAUDE_HOOK_COMMAND: &str = "rtk hook claude";
/// Shell-free Claude Code registration written to `command` and `args`.
pub const CLAUDE_HOOK_BINARY: &str = "rtk";
pub const CLAUDE_HOOK_ARGS: [&str; 2] = ["hook", "claude"];
/// Native Rust hook command for Cursor (replaces rtk-rewrite.sh).
pub const CURSOR_HOOK_COMMAND: &str = "rtk hook cursor";
/// Native Rust hook command for Factory Droid.
Expand Down
29 changes: 26 additions & 3 deletions src/hooks/hook_check.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

use super::constants::{HOOKS_SUBDIR, PRE_TOOL_USE_KEY, REWRITE_HOOK_FILE, SETTINGS_JSON};
use super::init::resolve_claude_dir;
use super::is_claude_hook_command;
use super::is_claude_hook_entry;
use crate::core::constants::RTK_DATA_DIR;
use std::path::PathBuf;

Expand Down Expand Up @@ -80,8 +80,7 @@ fn binary_hook_registered(claude_dir: &std::path::Path) -> bool {
.iter()
.filter_map(|entry| entry.get("hooks")?.as_array())
.flatten()
.filter_map(|hook| hook.get("command")?.as_str())
.any(is_claude_hook_command)
.any(is_claude_hook_entry)
}

/// Check if the installed hook is missing or outdated, warn once per day.
Expand Down Expand Up @@ -233,6 +232,30 @@ mod tests {
assert!(binary_hook_registered(tmp.path()));
}

#[test]
fn test_binary_hook_registered_accepts_windows_exec_form() {
let tmp = tempfile::tempdir().expect("tempdir");
std::fs::write(
tmp.path().join(SETTINGS_JSON),
r#"{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "C:\\Users\\me\\.local\\bin\\rtk.exe",
"args": ["hook", "claude"],
"timeout": 10
}]
}]
}
}"#,
)
.expect("write settings");

assert!(binary_hook_registered(tmp.path()));
}

#[test]
fn test_other_integration_none() {
let tmp = tempfile::tempdir().expect("tempdir");
Expand Down
Loading