Skip to content
Closed
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
69 changes: 60 additions & 9 deletions hooks/claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,74 @@

> Part of [`hooks/`](../README.md) — see also [`src/hooks/`](../../src/hooks/README.md) for installation code

## Specifics
Two hook implementations are provided. Both are thin delegates: they parse the
Claude Code JSON input, call `rtk rewrite`, and return the result in the
`updatedInput` format. All rewrite logic lives in the Rust binary.

- Shell-based `PreToolUse` hook -- requires `jq` for JSON parsing
## rtk-rewrite.sh (Linux / macOS)

- Requires `bash` and `jq`
- Installed automatically by `rtk init -g` on Unix systems
- Returns `updatedInput` JSON for transparent command rewrite (agent doesn't know RTK is involved)
- Exits silently (exit 0) on any failure: jq missing, rtk missing, rtk too old (< 0.23.0), no match
- Version guard checks `rtk --version` against minimum 0.23.0
- `rtk-awareness.md` is a slim 10-line instructions file embedded into CLAUDE.md by `rtk init`

## rtk-rewrite.py (Windows / cross-platform)

- Requires Python 3 (no third-party packages, no `jq`)
- Drop-in equivalent of `rtk-rewrite.sh` — identical exit code protocol and JSON output
- Works on Windows via Claude Code's Git Bash environment (`settings.json` hook)
- Same graceful degradation: exits 0 on all error paths so commands always run

### Manual installation on Windows

```powershell
# 1. Copy the hook
Copy-Item hooks\claude\rtk-rewrite.py "$env:USERPROFILE\.claude\hooks\rtk-rewrite.py"
```

Add the hook to `%USERPROFILE%\.claude\settings.json`:

```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python C:/Users/<you>/.claude/hooks/rtk-rewrite.py"
}
]
}
]
}
}
```

Then restart Claude Code. Commands like `git status` will be transparently
rewritten to `rtk git status` before execution.

## Testing

```bash
# Run the full test suite (60+ assertions)
bash hooks/test-rtk-rewrite.sh
# Shell hook — full test suite (60+ assertions)
bash hooks/claude/test-rtk-rewrite.sh

# Shell hook — test against a specific path
HOOK=/path/to/rtk-rewrite.sh bash hooks/claude/test-rtk-rewrite.sh

# Test against a specific hook path
HOOK=/path/to/rtk-rewrite.sh bash hooks/test-rtk-rewrite.sh
# Python hook — same assertions, cross-platform
python hooks/claude/test-rtk-rewrite.py

# Enable audit logging during testing
RTK_HOOK_AUDIT=1 RTK_AUDIT_DIR=/tmp bash hooks/test-rtk-rewrite.sh
# Python hook — test against a specific path
HOOK=/path/to/rtk-rewrite.py python hooks/claude/test-rtk-rewrite.py
```

Both test scripts share the same test cases so regressions in either hook are caught.

## rtk-awareness.md

A slim 10-line instructions file embedded into `CLAUDE.md` by `rtk init`.
Used as a fallback on systems where the hook cannot be installed.
117 changes: 117 additions & 0 deletions hooks/claude/rtk-rewrite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env python3
# rtk-hook-version: 3
# RTK Claude Code hook — rewrites commands to use rtk for token savings.
# Windows-compatible alternative to rtk-rewrite.sh. Requires only Python 3 and rtk >= 0.23.0.
# No jq dependency.
#
# This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`,
# which is the single source of truth (src/discover/registry.rs).
# To add or change rewrite rules, edit the Rust registry — not this file.
#
# Exit code protocol for `rtk rewrite`:
# 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow
# 1 No RTK equivalent → pass through unchanged
# 2 Deny rule matched → pass through (Claude Code native deny handles it)
# 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user

import json
import subprocess
import sys

_MIN_VERSION = (0, 23, 0)


def _check_rtk():
"""Return an error string if rtk is missing or too old, else None."""
try:
result = subprocess.run(["rtk", "--version"], capture_output=True, text=True)
parts = result.stdout.strip().split()
if len(parts) >= 2:
try:
version = tuple(int(x) for x in parts[1].split(".")[:3])
if version < _MIN_VERSION:
return (
f"rtk {parts[1]} is too old (need >= 0.23.0). "
"Upgrade: cargo install rtk"
)
except ValueError:
pass
except FileNotFoundError:
return (
"rtk is not installed or not in PATH. "
"Install: https://github.com/rtk-ai/rtk#installation"
)
return None


def main():
err = _check_rtk()
if err:
print(f"[rtk] WARNING: {err}", file=sys.stderr)
sys.exit(0)

try:
data = json.load(sys.stdin)
except Exception:
sys.exit(0)

cmd = data.get("tool_input", {}).get("command", "")
if not cmd:
sys.exit(0)

# Delegate all rewrite + permission logic to the Rust binary.
try:
result = subprocess.run(
["rtk", "rewrite", cmd],
capture_output=True,
text=True,
)
except Exception:
sys.exit(0)

exit_code = result.returncode
rewritten = result.stdout.strip()

if exit_code == 0:
# Rewrite found — if output is identical, command already uses RTK.
if cmd == rewritten:
sys.exit(0)
elif exit_code == 1:
# No RTK equivalent — pass through unchanged.
sys.exit(0)
elif exit_code == 2:
# Deny rule matched — let Claude Code's native deny rule handle it.
sys.exit(0)
elif exit_code == 3:
# Ask rule matched — rewrite but do NOT auto-allow so Claude Code prompts.
pass
else:
sys.exit(0)

updated_input = dict(data.get("tool_input", {}))
updated_input["command"] = rewritten

if exit_code == 3:
# Ask: omit permissionDecision so Claude Code prompts the user.
out = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": updated_input,
}
}
else:
# Allow: rewrite and auto-allow.
out = {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": updated_input,
}
}

print(json.dumps(out))


if __name__ == "__main__":
main()
Loading