Skip to content

Comments

feat: rtk rewrite — single source of truth for LLM hook rewrites#241

Open
FlorianBruniaux wants to merge 3 commits intomasterfrom
feat/rtk-rewrite
Open

feat: rtk rewrite — single source of truth for LLM hook rewrites#241
FlorianBruniaux wants to merge 3 commits intomasterfrom
feat/rtk-rewrite

Conversation

@FlorianBruniaux
Copy link
Collaborator

Problem

Every LLM hook (Claude Code, Gemini CLI, future Copilot...) was duplicating the same command-mapping logic in bash. Adding a new RTK filter meant updating every hook independently.

Thomas Delalonde identified this exactly in a community message:

"toute la partie pour créer la variable REWRITTEN pourrait être encapsulée dans RTK, afin de faciliter sa mise à jour avec de nouvelles commandes au besoin, ça réduit drastiquement le rtk_rewrite, et permet de le concentrer uniquement sur la partie proxy avec le modèle"

He was right. We had 3 independent copies of the same mapping:

  • .claude/hooks/rtk-rewrite.sh — 357 lines of bash grep/sed
  • src/discover/registry.rs — 21 Rust regex patterns (for rtk discover)
  • Thomas's Gemini CLI hook — ~120 lines of bash grep/sed

Solution: rtk rewrite <cmd>

A new CLI command that acts as the canonical rewrite engine for all hook integrations.

# Simple command
$ rtk rewrite "git status"
rtk git status

# Compound command — each segment rewritten independently
$ rtk rewrite "git add . && cargo test"
rtk git add . && rtk cargo test

# Pipe — only first segment rewritten
$ rtk rewrite "git log -10 | grep feat"
rtk git log -10 | grep feat

# Already RTK — exit 0, identical output
$ rtk rewrite "rtk git status"
rtk git status

# Unsupported — exit 1, no output (hook passes through unchanged)
$ rtk rewrite "terraform plan"
(exit 1)

Contract:

  • exit 0 + stdout = rewritten command (or already RTK, identical output)
  • exit 1 = no match, hook should pass through unchanged
  • No JSON, no formatting — just the raw command string

Impact on hooks

Every LLM hook now reduces to a single line:

REWRITTEN=$(rtk rewrite "$CMD") || exit 0

Claude Code hook: 357 lines → 60 lines
Thomas's Gemini CLI hook: ~120 lines → ~15 lines

When a new RTK filter is added (e.g., rtk ruff), all hooks update automatically — zero bash changes required.

Implementation

src/discover/registry.rs

  • Added rewrite_prefixes: &'static [&'static str] to RtkRule struct (ordered longest-first for correct matching)
  • All 21 rules updated with their rewrite prefix arrays
  • New public rewrite_command(cmd: &str) -> Option<String> — top-level entry point
  • Compound command handling: walks bytes, splits on &&/||/;/| (outside quotes), reconstructs with original separators
  • Reuses existing classify_command() for ignore/prefix handling (no duplicate IGNORED_PREFIXES logic)

src/rewrite_cmd.rs (new)

Thin CLI wrapper — Some → print + exit 0, Nonestd::process::exit(1)

src/main.rs

Commands::Rewrite { cmd } variant registered

.claude/hooks/rtk-rewrite.sh

Simplified from 357 → 60 lines. Single source of truth delegated to rtk rewrite.

Testing

20 new unit tests covering all branches:

  • Simple commands: git, cargo, gh, ls, cat, grep
  • Already-RTK passthrough
  • Compound &&, ||, ;
  • Pipe (first segment only)
  • Mixed compound (partial rewrite)
  • Ignored commands (cd, echo)
  • Unsupported commands (terraform)
  • Edge cases: empty input, heredocs, env var prefixes
436 tests passed, 0 failed

Gemini CLI hook template

Thomas's hook can now be reduced to:

#!/bin/bash
if ! command -v rtk &>/dev/null || ! command -v jq &>/dev/null; then exit 0; fi
set -euo pipefail

INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
[ -z "$CMD" ] && exit 0

REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || exit 0
[ "$CMD" = "$REWRITTEN" ] && exit 0

ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')
jq -n --argjson updated "$UPDATED_INPUT" \
  '{"hookSpecificOutput":{"decision":"allow","reason":"RTK auto-rewrite","tool_input":$updated}}'

A rtk init --gemini template is out of scope for this PR (tracked separately).


Inspired by Thomas Delalonde's community feedback — merci Thomas.

…rites

Implements `rtk rewrite <cmd>` as the canonical rewrite engine for all
LLM hook integrations (Claude Code, Gemini CLI, future tools).

- Add `rewrite_prefixes` field to `RtkRule` in discover/registry.rs
- Add public `rewrite_command()` with compound command support (&&, ||, ;, |)
- Add `rewrite_segment()`, `rewrite_compound()`, `strip_word_prefix()` helpers
- Handle already-rtk commands (exit 0, identical output)
- Handle unsupported/ignored commands (exit 1, no output)
- Add 20 unit tests covering all branches and edge cases
- Create `src/rewrite_cmd.rs` thin CLI wrapper
- Register `Commands::Rewrite` in main.rs
- Simplify `.claude/hooks/rtk-rewrite.sh` from 357 → 60 lines

Hooks no longer need duplicate mapping logic — a single
`REWRITTEN=$(rtk rewrite "$CMD") || exit 0` handles everything.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 21, 2026 17:22
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces rtk rewrite, a new CLI command that serves as a centralized rewriting engine for LLM hooks (Claude Code, Gemini CLI). The goal is to eliminate duplicate command-mapping logic across multiple bash hooks by providing a single source of truth in Rust. When a new RTK filter is added, all hooks automatically support it without bash changes.

Changes:

  • Adds rtk rewrite <cmd> command that rewrites shell commands to their RTK equivalents (exits 0 with rewritten output, or exits 1 if unsupported)
  • Simplifies .claude/hooks/rtk-rewrite.sh from 357 lines to 60 lines by delegating rewrite logic to rtk rewrite
  • Extends src/discover/registry.rs with rewrite_prefixes field and rewrite functions that handle simple and compound commands (&&, ||, ;, |)

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
src/rewrite_cmd.rs New thin CLI wrapper that calls registry::rewrite_command and exits appropriately
src/main.rs Registers Commands::Rewrite variant with documentation
src/discover/registry.rs Adds rewrite_prefixes to RtkRule, implements rewrite_command/rewrite_compound/rewrite_segment/strip_word_prefix functions, adds 19 unit tests
.claude/hooks/rtk-rewrite.sh Simplified to single rtk rewrite call, removes 300+ lines of duplicate bash patterns
Comments suppressed due to low confidence (3)

src/discover/registry.rs:137

  • The rewrite_prefixes for "rtk read" include "head" and "tail", but this loses the special argument transformation that existed in the old bash hook.

The old hook converted:

  • head -10 file.txtrtk read file.txt --max-lines 10
  • head --lines=10 file.txtrtk read file.txt --max-lines 10

The new implementation would convert:

  • head -10 file.txtrtk read -10 file.txt (INVALID - rtk read doesn't understand -N syntax)

This means head/tail commands will be "rewritten" to invalid rtk read commands that will fail at execution time.

Consider either:

  1. Remove "head" and "tail" from rewrite_prefixes (let them pass through unchanged)
  2. Add logic to rewrite_segment to handle argument transformation for head/tail
  3. Update rtk read to accept head/tail-style arguments like -N

Option 1 is simplest and most consistent with the PR's philosophy of simple prefix replacement without complex transformations.

    RtkRule {
        rtk_cmd: "rtk read",
        rewrite_prefixes: &["cat", "head", "tail"],
        category: "Files",
        savings_pct: 60.0,
        subcmd_savings: &[],
        subcmd_status: &[],

src/discover/registry.rs:260

  • This PR removes support for several RTK commands that exist in the codebase but are missing from the RULES array. The following RTK commands have implementations (confirmed by src/ directory listing) but are not included in PATTERNS or RULES:
  • rtk pytest (src/pytest_cmd.rs exists)
  • rtk ruff (src/ruff_cmd.rs exists)
  • rtk pip (src/pip_cmd.rs exists)
  • rtk go (src/go_cmd.rs exists)
  • rtk golangci-lint (src/golangci_cmd.rs exists)

These commands were supported in the old bash hook (visible in the removed lines of .claude/hooks/rtk-rewrite.sh) but are now missing. This means users who were relying on these rewrites will experience a regression - their commands will no longer be automatically rewritten by LLM hooks.

To fix this, add entries to both PATTERNS and RULES arrays for each missing command, following the same pattern as existing entries. For example, for pytest:

  • Add pattern: r"^(python3?\s+-m\s+)?pytest(\s|$)"
  • Add rule with rewrite_prefixes: &["python -m pytest", "python3 -m pytest", "pytest"]
const PATTERNS: &[&str] = &[
    r"^git\s+(status|log|diff|show|add|commit|push|pull|branch|fetch|stash|worktree)",
    r"^gh\s+(pr|issue|run|repo|api)",
    r"^cargo\s+(build|test|clippy|check|fmt)",
    r"^pnpm\s+(list|ls|outdated|install)",
    r"^npm\s+(run|exec)",
    r"^npx\s+",
    r"^(cat|head|tail)\s+",
    r"^(rg|grep)\s+",
    r"^ls(\s|$)",
    r"^find\s+",
    r"^(npx\s+|pnpm\s+)?tsc(\s|$)",
    r"^(npx\s+|pnpm\s+)?(eslint|biome|lint)(\s|$)",
    r"^(npx\s+|pnpm\s+)?prettier",
    r"^(npx\s+|pnpm\s+)?next\s+build",
    r"^(pnpm\s+|npx\s+)?(vitest|jest|test)(\s|$)",
    r"^(npx\s+|pnpm\s+)?playwright",
    r"^(npx\s+|pnpm\s+)?prisma",
    r"^docker\s+(ps|images|logs)",
    r"^kubectl\s+(get|logs)",
    r"^curl\s+",
    r"^wget\s+",
];

const RULES: &[RtkRule] = &[
    RtkRule {
        rtk_cmd: "rtk git",
        rewrite_prefixes: &["git"],
        category: "Git",
        savings_pct: 70.0,
        subcmd_savings: &[
            ("diff", 80.0),
            ("show", 80.0),
            ("add", 59.0),
            ("commit", 59.0),
        ],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk gh",
        rewrite_prefixes: &["gh"],
        category: "GitHub",
        savings_pct: 82.0,
        subcmd_savings: &[("pr", 87.0), ("run", 82.0), ("issue", 80.0)],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk cargo",
        rewrite_prefixes: &["cargo"],
        category: "Cargo",
        savings_pct: 80.0,
        subcmd_savings: &[("test", 90.0), ("check", 80.0)],
        subcmd_status: &[("fmt", super::report::RtkStatus::Passthrough)],
    },
    RtkRule {
        rtk_cmd: "rtk pnpm",
        rewrite_prefixes: &["pnpm"],
        category: "PackageManager",
        savings_pct: 80.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk npm",
        rewrite_prefixes: &["npm"],
        category: "PackageManager",
        savings_pct: 70.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk npx",
        rewrite_prefixes: &["npx"],
        category: "PackageManager",
        savings_pct: 70.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk read",
        rewrite_prefixes: &["cat", "head", "tail"],
        category: "Files",
        savings_pct: 60.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk grep",
        rewrite_prefixes: &["rg", "grep"],
        category: "Files",
        savings_pct: 75.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk ls",
        rewrite_prefixes: &["ls"],
        category: "Files",
        savings_pct: 65.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk find",
        rewrite_prefixes: &["find"],
        category: "Files",
        savings_pct: 70.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        // Longest prefixes first for correct matching
        rtk_cmd: "rtk tsc",
        rewrite_prefixes: &["pnpm tsc", "npx tsc", "tsc"],
        category: "Build",
        savings_pct: 83.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk lint",
        rewrite_prefixes: &[
            "npx eslint",
            "pnpm lint",
            "npx biome",
            "eslint",
            "biome",
            "lint",
        ],
        category: "Build",
        savings_pct: 84.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk prettier",
        rewrite_prefixes: &["npx prettier", "pnpm prettier", "prettier"],
        category: "Build",
        savings_pct: 70.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        // "next build" is stripped to "rtk next" — the build subcommand is internal
        rtk_cmd: "rtk next",
        rewrite_prefixes: &["npx next build", "pnpm next build", "next build"],
        category: "Build",
        savings_pct: 87.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk vitest",
        rewrite_prefixes: &["pnpm vitest", "npx vitest", "vitest", "jest"],
        category: "Tests",
        savings_pct: 99.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk playwright",
        rewrite_prefixes: &["npx playwright", "pnpm playwright", "playwright"],
        category: "Tests",
        savings_pct: 94.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk prisma",
        rewrite_prefixes: &["npx prisma", "pnpm prisma", "prisma"],
        category: "Build",
        savings_pct: 88.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk docker",
        rewrite_prefixes: &["docker"],
        category: "Infra",
        savings_pct: 85.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk kubectl",
        rewrite_prefixes: &["kubectl"],
        category: "Infra",
        savings_pct: 85.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk curl",
        rewrite_prefixes: &["curl"],
        category: "Network",
        savings_pct: 70.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
    RtkRule {
        rtk_cmd: "rtk wget",
        rewrite_prefixes: &["wget"],
        category: "Network",
        savings_pct: 65.0,
        subcmd_savings: &[],
        subcmd_status: &[],
    },
];

src/discover/registry.rs:210

  • The rewrite_prefixes array is incomplete - it doesn't cover all command forms that the PATTERN matches.

The PATTERN r"^(pnpm\s+|npx\s+)?(vitest|jest|test)(\s|$)" matches:

  • "pnpm test"
  • "npx test"
  • "test"
  • "npx jest"
  • "pnpm jest"

But rewrite_prefixes only includes: "pnpm vitest", "npx vitest", "vitest", "jest"

This means commands like "pnpm test" will match the pattern (classify as supported) but fail to rewrite because "pnpm test" isn't in the prefix list.

Additionally, the old bash hook converted "pnpm test" to "rtk vitest run" (with the "run" subcommand), but the current implementation would convert it to "rtk vitest" without a subcommand. Since VitestCommands requires a subcommand (line 745-752 of main.rs shows only a "Run" variant), "rtk vitest" without arguments would fail.

Add missing prefixes and consider whether "run" should be appended: rewrite_prefixes: &["pnpm vitest", "npx vitest", "pnpm test", "npx test", "vitest", "jest", "test"]

    RtkRule {
        rtk_cmd: "rtk vitest",
        rewrite_prefixes: &["pnpm vitest", "npx vitest", "vitest", "jest"],
        category: "Tests",
        savings_pct: 99.0,
        subcmd_savings: &[],
        subcmd_status: &[],

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

rewrite_command("rtk git add . && cargo test"),
Some("rtk git add . && rtk cargo test".into())
);
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

Test coverage is missing for the || (OR) operator in compound commands. While the code handles || (lines 576-589), there's no test verifying this works correctly.

Add a test like:

#[test]
fn test_rewrite_compound_or() {
    assert_eq!(
        rewrite_command("git fetch || git pull"),
        Some("rtk git fetch || rtk git pull".into())
    );
}
Suggested change
}
}
#[test]
fn test_rewrite_compound_or() {
assert_eq!(
rewrite_command("git fetch || git pull"),
Some("rtk git fetch || rtk git pull".into())
);
}

Copilot uses AI. Check for mistakes.
rewrite_command("rtk git add . && cargo test"),
Some("rtk git add . && rtk cargo test".into())
);
}
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

Test coverage is missing for the ; (semicolon) separator in compound commands. While the code handles ; (lines 619-635), there's no test verifying this works correctly.

Add a test like:

#[test]
fn test_rewrite_compound_semicolon() {
    assert_eq!(
        rewrite_command("git add .; cargo test"),
        Some("rtk git add .; rtk cargo test".into())
    );
}
Suggested change
}
}
#[test]
fn test_rewrite_compound_semicolon() {
assert_eq!(
rewrite_command("git add .; cargo test"),
Some("rtk git add .; rtk cargo test".into())
);
}

Copilot uses AI. Check for mistakes.
Comment on lines 173 to +181
rtk_cmd: "rtk lint",
rewrite_prefixes: &[
"npx eslint",
"pnpm lint",
"npx biome",
"eslint",
"biome",
"lint",
],
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The rewrite_prefixes array is incomplete - it doesn't cover all command forms that the PATTERN regex matches.

The PATTERN r"^(npx\s+|pnpm\s+)?(eslint|biome|lint)(\s|$)" matches commands like:

  • "pnpm eslint"
  • "npx lint"
  • "pnpm biome"

But rewrite_prefixes only includes:

  • "npx eslint", "pnpm lint", "npx biome", "eslint", "biome", "lint"

Missing: "pnpm eslint", "npx lint", "pnpm biome check", "eslint check", "biome check" etc.

When a command like "pnpm eslint src/" is encountered:

  1. classify_command matches it (PATTERN matches)
  2. rewrite_segment tries all prefixes but none match "pnpm eslint"
  3. Returns None - rewrite silently fails despite command being "supported"

Add missing prefix combinations to ensure all matched patterns can be rewritten:
rewrite_prefixes: &["npx eslint", "pnpm eslint", "npx biome", "pnpm biome", "pnpm lint", "npx lint", "eslint", "biome", "lint"]

Copilot uses AI. Check for mistakes.
Comment on lines 37 to 54
*'<<'*) _rtk_audit_log "skip:heredoc" "$CMD"; exit 0 ;;
esac

# 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 "")
if [ -n "$ENV_PREFIX" ]; then
MATCH_CMD="${FIRST_CMD:${#ENV_PREFIX}}"
CMD_BODY="${CMD:${#ENV_PREFIX}}"
else
MATCH_CMD="$FIRST_CMD"
CMD_BODY="$CMD"
fi

REWRITTEN=""

# --- Git commands ---
if echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+status([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git status/rtk git status/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+diff([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git diff/rtk git diff/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+log([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git log/rtk git log/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+add([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git add/rtk git add/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+commit([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git commit/rtk git commit/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+push([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git push/rtk git push/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+pull([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git pull/rtk git pull/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+branch([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git branch/rtk git branch/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+fetch([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git fetch/rtk git fetch/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+stash([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git stash/rtk git stash/')"
elif echo "$MATCH_CMD" | grep -qE '^git[[:space:]]+show([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^git show/rtk git show/')"

# --- GitHub CLI (added: api, release) ---
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 /')"

# --- Cargo ---
elif echo "$MATCH_CMD" | grep -qE '^cargo[[:space:]]+test([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cargo test/rtk cargo test/')"
elif echo "$MATCH_CMD" | grep -qE '^cargo[[:space:]]+build([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cargo build/rtk cargo build/')"
elif echo "$MATCH_CMD" | grep -qE '^cargo[[:space:]]+clippy([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cargo clippy/rtk cargo clippy/')"
elif echo "$MATCH_CMD" | grep -qE '^cargo[[:space:]]+check([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cargo check/rtk cargo check/')"
elif echo "$MATCH_CMD" | grep -qE '^cargo[[:space:]]+install([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cargo install/rtk cargo install/')"
elif echo "$MATCH_CMD" | grep -qE '^cargo[[:space:]]+nextest([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cargo nextest/rtk cargo nextest/')"
elif echo "$MATCH_CMD" | grep -qE '^cargo[[:space:]]+fmt([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cargo fmt/rtk cargo fmt/')"

# --- File operations ---
elif echo "$MATCH_CMD" | grep -qE '^cat[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^cat /rtk read /')"
elif echo "$MATCH_CMD" | grep -qE '^(rg|grep)[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(rg|grep) /rtk grep /')"
elif echo "$MATCH_CMD" | grep -qE '^ls([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^ls/rtk ls/')"
elif echo "$MATCH_CMD" | grep -qE '^tree([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^tree/rtk tree/')"
elif echo "$MATCH_CMD" | grep -qE '^find[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^find /rtk find /')"
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/')
REWRITTEN="${ENV_PREFIX}rtk read $FILE --max-lines $LINES"
elif echo "$MATCH_CMD" | grep -qE '^head[[:space:]]+--lines=[0-9]+[[:space:]]+'; then
LINES=$(echo "$MATCH_CMD" | sed -E 's/^head +--lines=([0-9]+) +.+$/\1/')
FILE=$(echo "$MATCH_CMD" | sed -E 's/^head +--lines=[0-9]+ +(.+)$/\1/')
REWRITTEN="${ENV_PREFIX}rtk read $FILE --max-lines $LINES"
fi

# --- JS/TS tooling (added: npm run, npm test, vue-tsc) ---
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
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pnpm test/rtk vitest run/')"
elif echo "$MATCH_CMD" | grep -qE '^npm[[:space:]]+test([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^npm test/rtk npm test/')"
elif echo "$MATCH_CMD" | grep -qE '^npm[[:space:]]+run[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^npm run /rtk npm /')"
elif echo "$MATCH_CMD" | grep -qE '^(npx[[:space:]]+)?vue-tsc([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(npx )?vue-tsc/rtk tsc/')"
elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+tsc([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pnpm tsc/rtk tsc/')"
elif echo "$MATCH_CMD" | grep -qE '^(npx[[:space:]]+)?tsc([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(npx )?tsc/rtk tsc/')"
elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+lint([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pnpm lint/rtk lint/')"
elif echo "$MATCH_CMD" | grep -qE '^(npx[[:space:]]+)?eslint([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(npx )?eslint/rtk lint/')"
elif echo "$MATCH_CMD" | grep -qE '^(npx[[:space:]]+)?prettier([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(npx )?prettier/rtk prettier/')"
elif echo "$MATCH_CMD" | grep -qE '^(npx[[:space:]]+)?playwright([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed -E 's/^(npx )?playwright/rtk playwright/')"
elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+playwright([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pnpm playwright/rtk playwright/')"
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) ---
elif echo "$MATCH_CMD" | grep -qE '^docker[[:space:]]+compose([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^docker /rtk docker /')"
elif echo "$MATCH_CMD" | grep -qE '^docker[[:space:]]+(ps|images|logs|run|build|exec)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^docker /rtk docker /')"
elif echo "$MATCH_CMD" | grep -qE '^kubectl[[:space:]]+(get|logs|describe|apply)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^kubectl /rtk kubectl /')"

# --- Network ---
elif echo "$MATCH_CMD" | grep -qE '^curl[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^curl /rtk curl /')"
elif echo "$MATCH_CMD" | grep -qE '^wget[[:space:]]+'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^wget /rtk wget /')"

# --- pnpm package management ---
elif echo "$MATCH_CMD" | grep -qE '^pnpm[[:space:]]+(list|ls|outdated)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pnpm /rtk pnpm /')"

# --- Python tooling ---
elif echo "$MATCH_CMD" | grep -qE '^pytest([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pytest/rtk pytest/')"
elif echo "$MATCH_CMD" | grep -qE '^python[[:space:]]+-m[[:space:]]+pytest([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^python -m pytest/rtk pytest/')"
elif echo "$MATCH_CMD" | grep -qE '^ruff[[:space:]]+(check|format)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^ruff /rtk ruff /')"
elif echo "$MATCH_CMD" | grep -qE '^pip[[:space:]]+(list|outdated|install|show)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^pip /rtk pip /')"
elif echo "$MATCH_CMD" | grep -qE '^uv[[:space:]]+pip[[:space:]]+(list|outdated|install|show)([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^uv pip /rtk pip /')"

# --- Go tooling ---
elif echo "$MATCH_CMD" | grep -qE '^go[[:space:]]+test([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^go test/rtk go test/')"
elif echo "$MATCH_CMD" | grep -qE '^go[[:space:]]+build([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^go build/rtk go build/')"
elif echo "$MATCH_CMD" | grep -qE '^go[[:space:]]+vet([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^go vet/rtk go vet/')"
elif echo "$MATCH_CMD" | grep -qE '^golangci-lint([[:space:]]|$)'; then
REWRITTEN="${ENV_PREFIX}$(echo "$CMD_BODY" | sed 's/^golangci-lint/rtk golangci-lint/')"
fi

# If no rewrite needed, approve as-is
if [ -z "$REWRITTEN" ]; then
# Rewrite via rtk — single source of truth for all command mappings.
# Exit 1 = no RTK equivalent, pass through unchanged.
# Exit 0 = rewritten command (or already RTK, identical output).
REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || {
_rtk_audit_log "skip:no_match" "$CMD"
exit 0
}

# If output is identical, command was already using RTK — nothing to do.
if [ "$CMD" = "$REWRITTEN" ]; then
_rtk_audit_log "skip:already_rtk" "$CMD"
exit 0
fi

_rtk_audit_log "rewrite" "$CMD" "$REWRITTEN"
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The calls to _rtk_audit_log with "$CMD" and "$REWRITTEN" persist the full command line and its rewritten form to a local log file, which can include secrets such as API tokens, passwords in URLs, or authorization headers. An attacker (or another local user, backups, or support channels receiving this log) could recover those secrets from ${HOME}/.local/share/rtk/hook-audit.log when RTK_HOOK_AUDIT=1. To reduce this risk, avoid logging raw command strings, or at least redact/strip sensitive arguments and ensure the log file has appropriately restricted permissions and a clear warning in documentation about potential secret exposure.

Copilot uses AI. Check for mistakes.
FlorianBruniaux and others added 2 commits February 21, 2026 18:31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- README.md, CLAUDE.md, ARCHITECTURE.md: 0.20.1 → 0.22.2
- ARCHITECTURE.md: module count 48 → 51 (added rewrite_cmd + 2 from master)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant