Skip to content
Open
8 changes: 8 additions & 0 deletions .github/workflows/security-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,14 @@ jobs:
fi
echo "" >> $GITHUB_STEP_SUMMARY

- name: Build
run: cargo build

- name: Smoke Tests
run: |
export PATH=$PATH:$(pwd)/target/debug
./scripts/test-all.sh

- name: Summary verdict
run: |
echo "---" >> $GITHUB_STEP_SUMMARY
Expand Down
11 changes: 6 additions & 5 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ Database: ~/.local/share/rtk/history.db

## Module Organization

### Complete Module Map (30 Modules)
### Complete Module Map (48 Modules)

```
┌────────────────────────────────────────────────────────────────────────┐
Expand Down Expand Up @@ -240,12 +240,13 @@ SHARED utils.rs Helpers N/A ✓
tee.rs Full output recovery N/A ✓
```

**Total: 48 modules** (30 command modules + 18 infrastructure modules)

**Total: 48 modules** (31 command modules + 17 infrastructure modules)

### Module Count Breakdown

- **Command Modules**: 29 (directly exposed to users)
- **Infrastructure Modules**: 18 (utils, filter, tracking, tee, config, init, gain, etc.)
- **Infrastructure Modules**: 19 (utils, filter, tracking, tee, config, init, gain, etc.)
- **Git Commands**: 7 operations (status, diff, log, add, commit, push, branch/checkout)
- **JS/TS Tooling**: 8 modules (modern frontend/fullstack development)
- **Python Tooling**: 3 modules (ruff, pytest, pip)
Expand Down Expand Up @@ -1433,6 +1434,6 @@ When implementing a new command, consider:

---

**Last Updated**: 2026-02-12
**Last Updated**: 2026-02-17
**Architecture Version**: 2.1
**rtk Version**: 0.20.1
**rtk Version**: 0.21.1
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ This is a fork with critical fixes for git argument parsing and modern JavaScrip

**Verify correct installation:**
```bash
rtk --version # Should show "rtk 0.20.1" (or newer)
rtk --version # Should show "rtk 0.21.1" (or newer)
rtk gain # Should show token savings stats (NOT "command not found")
```

Expand Down
21 changes: 20 additions & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,26 @@ rtk init -g --no-patch # Print manual instructions instead
rtk init --show # Check hook is installed and executable
```

**Token savings**: ~99.5% reduction (2000 tokens → 10 tokens in context)
### Gemini CLI Setup

RTK integrates with Gemini CLI via a **BeforeTool hook** that automatically rewrites commands:

```bash
rtk init -g --gemini
# → Installs ~/.gemini/hooks/rtk-rewrite.sh
# → Creates ~/.gemini/RTK.md (command reference)
# → Creates ~/.gemini/GEMINI.md (usage guide)
# → Patches ~/.gemini/settings.json (registers hook)
```

**Verify installation:**
```bash
gemini /hooks # Should show "rtk-rewrite" under BeforeTool
```

**Token savings**: 70-90% reduction on git, npm, file operations.

**How it works**: The hook intercepts `run_shell_command` tool calls and rewrites them to their `rtk` equivalents before execution. Gemini never sees the original command.

**What is settings.json?**
Claude Code's hook registry. RTK adds a PreToolUse hook that rewrites commands transparently. Without this, Claude won't invoke the hook automatically.
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ rtk filters and compresses command outputs before they reach your LLM context, s

**How to verify you have the correct rtk:**
```bash
rtk --version # Should show "rtk 0.20.1"
rtk --version # Should show "rtk 0.21.1"
rtk gain # Should show token savings stats
```

Expand Down Expand Up @@ -112,10 +112,12 @@ Download from [rtk-ai/releases](https://github.com/rtk-ai/rtk/releases):
# 1. Verify installation
rtk gain # Must show token stats, not "command not found"

# 2. Initialize for Claude Code (RECOMMENDED: hook-first mode)
rtk init --global
# 2. Initialize (RECOMMENDED: hook-first mode)
rtk init --global # For Claude Code
Copy link

@bleleve bleleve Feb 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep it understandable/simple in six months, I will have taken this approach:

  • rtk init --claude
  • rtk init --gemini
  • rtk init --global -> auto CLIs detection (search for ~/.gemini and ~/.claude)

(By the way, Hey Ousama, I hope you are doing well 😄)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a PR in progress similar to this one.
#158

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey! I’m doing great, hope you are too 🙂

I’m aligned with your approach and implementing it in a lightweight way.

There was #131, but it was considered too large and needed splitting.
So I decided to move forward with a lighter, more focused version instead.

I’ve also applied the requested fixes:
feat: auto-detect CLIs with rtk init --global, add --claude flag

I’m against duplicate work… just like I’m against copy-paste code 😄
Happy to align if #158 already covers it.

rtk init --global --gemini # For Gemini CLI
# → Installs hook + creates slim RTK.md (10 lines, 99.5% token savings)
# → Follow printed instructions to add hook to ~/.claude/settings.json
# → For Claude: Follow instructions to patch ~/.claude/settings.json
# → For Gemini: Automatically patches ~/.gemini/settings.json

# 3. Test it works
rtk git status # Should show ultra-compact output
Expand Down
64 changes: 29 additions & 35 deletions hooks/rtk-rewrite.sh
Original file line number Diff line number Diff line change
@@ -1,45 +1,44 @@
#!/bin/bash
# RTK auto-rewrite hook for Claude Code PreToolUse:Bash
#!/usr/bin/env bash
# hooks/rtk-rewrite.sh
# RTK auto-rewrite hook for Gemini CLI BeforeTool
# Transparently rewrites raw commands to their rtk equivalents.
# Outputs JSON with updatedInput to modify the command before execution.

# Guards: skip silently if dependencies missing
if ! command -v rtk &>/dev/null || ! command -v jq &>/dev/null; then
echo '{"decision": "allow"}'
exit 0
fi

set -euo pipefail

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

if [ -z "$CMD" ]; then
if [ "$TOOL_NAME" != "run_shell_command" ] || [ -z "$CMD" ]; then
echo '{"decision": "allow"}'
exit 0
fi

# Extract the first meaningful command (before pipes, &&, etc.)
# We only rewrite if the FIRST command in a chain matches.
FIRST_CMD="$CMD"

# Skip if already using rtk
case "$FIRST_CMD" in
rtk\ *|*/rtk\ *) exit 0 ;;
esac
if [[ "$CMD" =~ ^rtk\ ]] || [[ "$CMD" =~ ^.*/rtk\ ]]; then
echo '{"decision": "allow"}'
exit 0
fi

# Skip commands with heredocs, variable assignments as the whole command, etc.
case "$FIRST_CMD" in
*'<<'*) exit 0 ;;
esac
# Skip commands with heredocs
if [[ "$CMD" == *"<<"* ]]; then
echo '{"decision": "allow"}'
exit 0
fi

# Strip leading env var assignments for pattern matching
# e.g., "TEST_SESSION_ID=2 npx playwright test" → match against "npx playwright test"
# but preserve them in the rewritten command for execution.
ENV_PREFIX=$(echo "$FIRST_CMD" | grep -oE '^([A-Za-z_][A-Za-z0-9_]*=[^ ]* +)+' || echo "")
ENV_PREFIX=$(echo "$CMD" | grep -oE '^([A-Za-z_][A-Za-z0-9_]*=[^ ]* +)+' || echo "")
if [ -n "$ENV_PREFIX" ]; then
MATCH_CMD="${FIRST_CMD:${#ENV_PREFIX}}"
MATCH_CMD="${CMD:${#ENV_PREFIX}}"
CMD_BODY="${CMD:${#ENV_PREFIX}}"
else
MATCH_CMD="$FIRST_CMD"
MATCH_CMD="$CMD"
CMD_BODY="$CMD"
fi

Expand All @@ -59,7 +58,7 @@ if echo "$MATCH_CMD" | grep -qE '^git[[:space:]]'; then
;;
esac

# --- GitHub CLI (added: api, release) ---
# --- GitHub CLI ---
elif echo "$MATCH_CMD" | grep -qE '^gh[[:space:]]+(pr|issue|run|api|release)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^gh /rtk gh /')"

Expand All @@ -86,8 +85,6 @@ elif echo "$MATCH_CMD" | grep -qE '^find[[:space:]]+'; then
elif echo "$MATCH_CMD" | grep -qE '^diff[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^diff /rtk diff /')"
elif echo "$MATCH_CMD" | grep -qE '^head[[:space:]]+'; then
# Transform: head -N file → rtk read file --max-lines N
# Also handle: head --lines=N file
if echo "$MATCH_CMD" | grep -qE '^head[[:space:]]+-[0-9]+[[:space:]]+'; then
LINES=$(echo "$MATCH_CMD" | sed -E 's/^head +-([0-9]+) +.+$/\1/')
FILE=$(echo "$MATCH_CMD" | sed -E 's/^head +-[0-9]+ +(.+)$/\1/')
Expand All @@ -98,7 +95,7 @@ elif echo "$MATCH_CMD" | grep -qE '^head[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}rtk read $FILE --max-lines $LINES"
fi

# --- JS/TS tooling (added: npm run, npm test, vue-tsc) ---
# --- JS/TS tooling ---
elif echo "$MATCH_CMD" | grep -qE '^(pnpm[[:space:]]+)?(npx[[:space:]]+)?vitest([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(pnpm )?(npx )?vitest( run)?/rtk vitest run/')"
elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+test([[:space:]]|$)'; then
Expand Down Expand Up @@ -126,7 +123,7 @@ elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+playwright([[:space:]]|$)';
elif echo "$MATCH_CMD" | grep -qE '^(npx[[:space:]]+)?prisma([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(npx )?prisma/rtk prisma/')"

# --- Containers (added: docker compose, docker run/build/exec, kubectl describe/apply) ---
# --- Containers ---
elif echo "$MATCH_CMD" | grep -qE '^docker[[:space:]]'; then
if echo "$MATCH_CMD" | grep -qE '^docker[[:space:]]+compose([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^docker /rtk docker /')"
Expand Down Expand Up @@ -189,21 +186,18 @@ fi

# If no rewrite needed, approve as-is
if [ -z "$REWRITTEN" ]; then
echo '{"decision": "allow"}'
exit 0
fi

# Build the updated tool_input with all original fields preserved, only command changed
ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')

# Output the rewrite instruction
# Output the rewrite instruction in Gemini CLI format
jq -n \
--argjson updated "$UPDATED_INPUT" \
--arg cmd "$REWRITTEN" \
'{
"decision": "allow",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": $updated
"tool_input": {
"command": $cmd
}
}
}'
24 changes: 12 additions & 12 deletions hooks/test-rtk-rewrite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test_rewrite() {
TOTAL=$((TOTAL + 1))

local input_json
input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"Bash","tool_input":{"command":$cmd}}')
input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}')
local output
output=$(echo "$input_json" | bash "$HOOK" 2>/dev/null) || true

Expand All @@ -33,15 +33,15 @@ test_rewrite() {
PASS=$((PASS + 1))
else
local actual
actual=$(echo "$output" | jq -r '.hookSpecificOutput.updatedInput.command // empty')
actual=$(echo "$output" | jq -r '.hookSpecificOutput.tool_input.command // empty')
printf " ${RED}FAIL${RESET} %s\n" "$description"
printf " expected: (no rewrite)\n"
printf " actual: %s\n" "$actual"
FAIL=$((FAIL + 1))
fi
else
local actual
actual=$(echo "$output" | jq -r '.hookSpecificOutput.updatedInput.command // empty' 2>/dev/null)
actual=$(echo "$output" | jq -r '.hookSpecificOutput.tool_input.command // empty' 2>/dev/null)
if [ "$actual" = "$expected_cmd" ]; then
printf " ${GREEN}PASS${RESET} %s ${DIM}→ %s${RESET}\n" "$description" "$actual"
PASS=$((PASS + 1))
Expand Down Expand Up @@ -193,17 +193,17 @@ test_rewrite "docker exec -it db psql" \
"docker exec -it db psql" \
"rtk docker exec -it db psql"

test_rewrite "find (NOT rewritten — different arg format)" \
test_rewrite "find" \
"find . -name '*.ts'" \
""
"rtk find . -name '*.ts'"

test_rewrite "tree (NOT rewritten — different arg format)" \
test_rewrite "tree" \
"tree src/" \
""
"rtk tree src/"

test_rewrite "wget (NOT rewritten — different arg format)" \
test_rewrite "wget" \
"wget https://example.com/file" \
""
"rtk wget https://example.com/file"

test_rewrite "gh api repos/owner/repo" \
"gh api repos/owner/repo" \
Expand Down Expand Up @@ -297,7 +297,7 @@ test_audit_log() {
rm -f "$AUDIT_TMPDIR/hook-audit.log"

local input_json
input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"Bash","tool_input":{"command":$cmd}}')
input_json=$(jq -n --arg cmd "$input_cmd" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}')
echo "$input_json" | RTK_HOOK_AUDIT=1 RTK_AUDIT_DIR="$AUDIT_TMPDIR" bash "$HOOK" 2>/dev/null || true

if [ ! -f "$AUDIT_TMPDIR/hook-audit.log" ]; then
Expand Down Expand Up @@ -347,7 +347,7 @@ test_audit_log "audit: rewrite cargo test" \

# Test log format (4 pipe-separated fields)
rm -f "$AUDIT_TMPDIR/hook-audit.log"
input_json=$(jq -n --arg cmd "git status" '{"tool_name":"Bash","tool_input":{"command":$cmd}}')
input_json=$(jq -n --arg cmd "git status" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}')
echo "$input_json" | RTK_HOOK_AUDIT=1 RTK_AUDIT_DIR="$AUDIT_TMPDIR" bash "$HOOK" 2>/dev/null || true
TOTAL=$((TOTAL + 1))
log_line=$(cat "$AUDIT_TMPDIR/hook-audit.log" 2>/dev/null || echo "")
Expand All @@ -363,7 +363,7 @@ fi

# Test no log when RTK_HOOK_AUDIT is unset
rm -f "$AUDIT_TMPDIR/hook-audit.log"
input_json=$(jq -n --arg cmd "git status" '{"tool_name":"Bash","tool_input":{"command":$cmd}}')
input_json=$(jq -n --arg cmd "git status" '{"tool_name":"run_shell_command","tool_input":{"command":$cmd}}')
echo "$input_json" | RTK_AUDIT_DIR="$AUDIT_TMPDIR" bash "$HOOK" 2>/dev/null || true
TOTAL=$((TOTAL + 1))
if [ ! -f "$AUDIT_TMPDIR/hook-audit.log" ]; then
Expand Down
Loading