Skip to content
Closed
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
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ SHARED utils.rs Helpers N/A ✓
tee.rs Full output recovery N/A ✓
```

**Total: 60 modules** (38 command modules + 22 infrastructure modules)
**Total: 61 modules** (38 command modules + 23 infrastructure modules)

### Module Count Breakdown

Expand Down Expand Up @@ -1488,4 +1488,4 @@ When implementing a new command, consider:

**Last Updated**: 2026-02-22
**Architecture Version**: 2.2
**rtk Version**: 0.28.0
**rtk Version**: 0.28.2
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.28.0" (or newer)
rtk --version # Should show "rtk 0.28.2" (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 @@ -107,7 +107,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
18 changes: 13 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@

rtk filters and compresses command outputs before they reach your LLM context. Single Rust binary, zero dependencies, <10ms overhead.

> **Name collision warning:** There are TWO different "rtk" projects. This is **Rust Token Killer** (`rtk-ai/rtk`). If `rtk gain` doesn't work, you may have installed `reachingforthejack/rtk` (Rust Type Kit) instead.

## Token Savings (30-min Claude Code Session)

| Operation | Frequency | Standard | rtk | Savings |
Expand Down Expand Up @@ -90,7 +92,7 @@ Download from [releases](https://github.com/rtk-ai/rtk/releases):
### Verify Installation

```bash
rtk --version # Should show "rtk 0.28.0"
rtk --version # Should show "rtk 0.28.2"
rtk gain # Should show token savings stats
```

Expand All @@ -99,11 +101,17 @@ rtk gain # Should show token savings stats
## Quick Start

```bash
# 1. Install hook for Claude Code (recommended)
rtk init --global
# Follow instructions to register in ~/.claude/settings.json
# 1. Verify installation
rtk gain # Must show token stats, not "command not found"

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

@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
Copy Markdown

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
Copy Markdown
Contributor 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)
# → For Claude: Follow instructions to patch ~/.claude/settings.json
# → For Gemini: Automatically patches ~/.gemini/settings.json

# 2. Restart Claude Code, then test
# 3. Restart Claude Code / Gemini CLI, then test
git status # Automatically rewritten to rtk git status
```

Expand Down
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 @@ -209,17 +209,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 @@ -351,7 +351,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 @@ -401,7 +401,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 @@ -417,7 +417,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
69 changes: 67 additions & 2 deletions scripts/test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ assert_ok "rtk cargo build" rtk cargo build
assert_ok "rtk cargo clippy" rtk cargo clippy
# cargo test exits non-zero due to pre-existing failures; check output ignoring exit code
output_cargo_test=$(rtk cargo test 2>&1 || true)
if echo "$output_cargo_test" | grep -q "FAILURES\|test result:\|passed"; then
if echo "$output_cargo_test" | grep -qE "FAILURES|test result:|passed"; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "rtk cargo test"
else
Expand Down Expand Up @@ -346,6 +346,30 @@ section "Config & Init"
assert_ok "rtk config" rtk config
assert_ok "rtk init --show" rtk init --show

section "Init Gemini (global)"

# Setup temporary home for gemini test
TEST_HOME_GEMINI=$(mktemp -d)
OLD_HOME=$HOME
export HOME=$TEST_HOME_GEMINI

assert_ok "rtk init -g --gemini" rtk init -g --gemini
if [ -f "$HOME/.gemini/GEMINI.md" ] && [ -f "$HOME/.gemini/settings.json" ]; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "rtk init gemini files exist"
else
FAIL=$((FAIL + 1))
FAILURES+=("rtk init gemini files exist")
printf " ${RED}FAIL${NC} %s\n" "rtk init gemini files exist"
fi

# Cleanup
export HOME=$OLD_HOME
rm -rf "$TEST_HOME_GEMINI"

section "Auto Detect"
bash scripts/test-auto-detect.sh

# ── 22. Wget ─────────────────────────────────────────

section "Wget"
Expand Down Expand Up @@ -426,6 +450,43 @@ else
skip_test "rtk golangci-lint" "golangci-lint not installed"
fi

# ── 28b. Hook Gemini ──────────────────────────────

section "Hook Gemini"

HOOK_OUT=$(echo '{"tool_name":"run_shell_command","tool_input":{"command":"git status"}}' | rtk hook gemini 2>/dev/null)
if echo "$HOOK_OUT" | grep -q '"rtk git status"'; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "rtk hook gemini rewrites git status"
else
FAIL=$((FAIL + 1))
FAILURES+=("rtk hook gemini rewrites git status")
printf " ${RED}FAIL${NC} %s\n" "rtk hook gemini rewrites git status"
printf " got: %s\n" "$HOOK_OUT"
fi

HOOK_OUT2=$(echo '{"tool_name":"run_shell_command","tool_input":{"command":"echo hello"}}' | rtk hook gemini 2>/dev/null)
if echo "$HOOK_OUT2" | grep -q '"decision":"allow"' && ! echo "$HOOK_OUT2" | grep -q 'hookSpecificOutput'; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "rtk hook gemini passthrough echo"
else
FAIL=$((FAIL + 1))
FAILURES+=("rtk hook gemini passthrough echo")
printf " ${RED}FAIL${NC} %s\n" "rtk hook gemini passthrough echo"
printf " got: %s\n" "$HOOK_OUT2"
fi

HOOK_OUT3=$(echo '{"tool_name":"run_shell_command","tool_input":{"command":"rtk git status"}}' | rtk hook gemini 2>/dev/null)
if echo "$HOOK_OUT3" | grep -q '"decision":"allow"' && ! echo "$HOOK_OUT3" | grep -q 'hookSpecificOutput'; then
PASS=$((PASS + 1))
printf " ${GREEN}PASS${NC} %s\n" "rtk hook gemini no double-rewrite"
else
FAIL=$((FAIL + 1))
FAILURES+=("rtk hook gemini no double-rewrite")
printf " ${RED}FAIL${NC} %s\n" "rtk hook gemini no double-rewrite"
printf " got: %s\n" "$HOOK_OUT3"
fi

# ── 29. Graphite (conditional) ─────────────────────

section "Graphite (conditional)"
Expand Down Expand Up @@ -455,7 +516,11 @@ assert_ok "rtk cc-economics" rtk cc-economics
section "Learn"

assert_ok "rtk learn --help" rtk learn --help
assert_ok "rtk learn (no sessions)" rtk learn --since 0 2>&1 || true
if [[ -d "$HOME/.claude/projects" ]]; then
assert_ok "rtk learn (no sessions)" rtk learn --since 0 2>&1 || true
else
skip_test "rtk learn (no sessions)" "Claude Code not installed"
fi

# ── 32. Rewrite ───────────────────────────────────────

Expand Down
68 changes: 68 additions & 0 deletions scripts/test-auto-detect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/bin/bash
set -e

RTK_BIN="./target/debug/rtk"
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
PASS=0
FAIL=0

HOME_SAV=$HOME
TEST_HOME=$(mktemp -d)
export HOME=$TEST_HOME
trap 'export HOME=$HOME_SAV; rm -rf "$TEST_HOME"' EXIT

check() {
local label=$1
local file=$2
if [ -f "$file" ] || [ -d "$file" ]; then
echo -e "${GREEN}✅ $label${NC}"
PASS=$((PASS + 1))
else
echo -e "${RED}❌ $label — not found: $file${NC}"
FAIL=$((FAIL + 1))
fi
}

# Ensure binary exists
if [ ! -f "$RTK_BIN" ]; then
echo "Building rtk..."
cargo build
fi

echo "--- Cas 1: Claude only ---"
mkdir -p "$HOME/.claude"
$RTK_BIN init -g --auto-patch > /dev/null
check "Claude: settings.json" "$HOME/.claude/settings.json"
check "Claude: hook" "$HOME/.claude/hooks/rtk-rewrite.sh"
rm -rf "$HOME/.claude" "$HOME/.gemini"

echo "--- Cas 2: Gemini only ---"
mkdir -p "$HOME/.gemini"
$RTK_BIN init -g --auto-patch > /dev/null
check "Gemini: settings.json" "$HOME/.gemini/settings.json"
check "Gemini: hook" "$HOME/.gemini/hooks/rtk-rewrite.sh"
check "Gemini: GEMINI.md" "$HOME/.gemini/GEMINI.md"
rm -rf "$HOME/.claude" "$HOME/.gemini"

echo "--- Cas 3: Both ---"
mkdir -p "$HOME/.claude" "$HOME/.gemini"
$RTK_BIN init -g --auto-patch > /dev/null
check "Both: Claude settings.json" "$HOME/.claude/settings.json"
check "Both: Gemini settings.json" "$HOME/.gemini/settings.json"
rm -rf "$HOME/.claude" "$HOME/.gemini"

echo "--- Cas 4: No CLI ---"
output=$($RTK_BIN init -g 2>&1 || true)
if echo "$output" | grep -q "No CLI detected"; then
echo -e "${GREEN}✅ No CLI: message correct${NC}"
PASS=$((PASS + 1))
else
echo -e "${RED}❌ No CLI: message manquant${NC}"
FAIL=$((FAIL + 1))
fi

echo ""
echo "Results: $PASS passed, $FAIL failed"
[ $FAIL -eq 0 ] && echo -e "${GREEN}✨ ALL PASSED ✨${NC}" || exit 1
Loading