Skip to content

Comments

feat(hook): native cross-platform (Windows & more) hook-rewrite command#150

Open
ayoub-khemissi wants to merge 6 commits intortk-ai:masterfrom
ayoub-khemissi:feat/native-hook-rewrite
Open

feat(hook): native cross-platform (Windows & more) hook-rewrite command#150
ayoub-khemissi wants to merge 6 commits intortk-ai:masterfrom
ayoub-khemissi:feat/native-hook-rewrite

Conversation

@ayoub-khemissi
Copy link

@ayoub-khemissi ayoub-khemissi commented Feb 16, 2026

Summary

Replace the bash-based rtk-rewrite.sh hook with a native Rust rtk hook-rewrite subcommand, eliminating the bash/jq dependency and enabling cross-platform support (Windows, macOS, Linux).

  • New src/hook_cmd.rs (1126 lines): Pure Rust command rewriter that reads Claude Code PreToolUse JSON from stdin, rewrites known commands to their rtk equivalents, and outputs updated JSON. Supports command chains (&&, ;), env var prefixes, git/cargo/docker/kubectl global flags, and 30+ command patterns (git, cargo, gh, pnpm, vitest, tsc, eslint, prettier, playwright, prisma, pytest, ruff, pip, go, golangci-lint, curl, wget, cat, grep, find, ls, tree, head, diff).
  • Updated src/init.rs: rtk init now installs the native rtk hook-rewrite command instead of the bash script. Includes migration logic to clean up old .sh hook files. Both old and new formats are detected during --uninstall.
  • Updated src/main.rs: Added hidden hook-rewrite subcommand routing to hook_cmd::run().
  • New build.rs: Set 8 MB stack size on Windows to prevent debug build stack overflow (Windows defaults to 1 MB vs 8 MB on Unix).
  • Minor src/gain.rs: Formatting fix (line wrapping for table_width calculation).

Why native?

Bash hook (rtk-rewrite.sh) Native hook (rtk hook-rewrite)
Dependencies bash + jq required None (single binary)
Windows Requires Git Bash/WSL Works natively
Performance Shell startup + jq parsing Direct Rust execution
Security Shell script attack surface Pure string manipulation, no shell spawning
Maintenance Separate script file to distribute Built into rtk binary

Security review

Per SECURITY.md checklist:

  • No Command::new("sh") — pure string rewriting, no shell spawning
  • No unsafe {} blocks
  • No network operations (reqwest::, std::net::)
  • No environment manipulation (.env("LD_PRELOAD"))
  • No .unwrap() in production paths (only in lazy_static! regex compilation)
  • Input validation via allowlists (specific subcommands matched, not denylists)
  • Graceful failure: any parse error → exit(0) (no rewrite, original command passes through)
  • init.rs changes reduce attack surface by removing bash script dependency

Test plan

  • 56 unit tests covering all rewrite patterns, edge cases, command chains, env prefixes, JSON roundtrip
  • cargo fmt --all --check && cargo clippy --all-targets && cargo test --all passes
  • Manual test: rtk init --auto-patch installs native hook command in settings.json
  • Manual test: rtk init --uninstall removes both old .sh and new native hook entries
  • Manual test: In Claude Code session, raw git status is rewritten to rtk git status
  • Cross-platform: Verify on Windows (no bash dependency needed)

Verification results (Windows 11, 2026-02-16)

Quality pipeline

Check Result
cargo fmt --all --check PASS — no diff
cargo clippy --all-targets PASS — 45 pre-existing warnings, 0 errors
cargo test --all PASS — 413 tests, 0 failures

Uninstall coverage

Scenario Result
Remove native hook (rtk hook-rewrite) from settings.json OK
Remove old bash hook (rtk-rewrite.sh) file OK
Remove RTK.md OK
Remove @RTK.md reference from CLAUDE.md OK
Migration .sh → native then uninstall OK — both cleaned up
Idempotent uninstall (nothing installed) OK — "RTK was not installed"

Hook-rewrite manual verification

Raw command Rewritten to
git status rtk git status
cargo test -- --nocapture rtk cargo test -- --nocapture
cd /project && git status && cargo test cd /project && rtk git status && rtk cargo test
TEST_ID=123 git status TEST_ID=123 rtk git status
cat src/main.rs rtk read src/main.rs
python -m pytest tests/ rtk pytest tests/
rtk git status (already prefixed) No double-rewrite
Non-Bash tool (Read) Silently ignored (exit 0)

Cross-platform (Windows)

  • Binary: Native PE64 executable, 5.4 MB stripped
  • Zero bash dependency: No bash, sh, jq, sed, grep calls in hook_cmd.rs
  • hook-rewrite: Pure Rust stdin/stdout JSON, works natively on Windows
  • Stack overflow fix: build.rs sets /STACK:8388608 (8 MB) on Windows via linker flag — zero runtime overhead, matches Unix defaults

🤖 Generated with Claude Code

ayoub-khemissi and others added 3 commits February 16, 2026 20:38
Windows default stack is 1 MB vs 8 MB on Unix. The large Commands enum
(30+ Clap variants) overflows in unoptimized debug builds. Add build.rs
with /STACK linker flag for Windows only, zero runtime overhead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep both hook-rewrite (this PR) and hook-audit (upstream rtk-ai#151) commands.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ayoub-khemissi ayoub-khemissi changed the title feat(hook): native cross-platform hook-rewrite command feat(hook): native cross-platform (Windows & more) hook-rewrite command Feb 17, 2026
@aeppling aeppling added the P1-critical Bloque des utilisateurs, fix ASAP label Feb 18, 2026
@aeppling
Copy link
Contributor

Hello,

Approved.

Code is correct for me, but for futures PR please split files into respective components to avoid having 1000+ lines files.

We should refactor the code structure of the project for a clean architecture that will help for easy reviewing and extensible code.

@pszymkowiak
Copy link
Collaborator

Thanks for this massive PR — the native hook-rewrite is a great step forward for cross-platform support!

I did an exhaustive test of 313 commands across all 12 categories, simulating the real Claude Code hook JSON format (tool_input.command). Here are the full results:

Test Results: 312/313 passed (99.7%)

Category Tested Passed Notes
Git (supported) 29 29 status, diff, log, add, commit, push, pull, branch, fetch, stash, show, worktree
Git (blocked) 15 15 checkout, merge, rebase, reset, cherry-pick, tag, bisect, reflog, remote, clean, blame, init, clone
Git (global flags) 7 7 -C, --no-pager, --bare, --no-optional-locks, -c key=val
GitHub CLI (supported) 17 17 pr, issue, run, api, release
GitHub CLI (blocked) 5 5 repo, gist, workflow, auth, config
Cargo (supported) 19 19 build, test, clippy, check, fmt, install, +toolchain
Cargo (blocked) 7 7 run, new, publish, doc, bench, update, add
Files 21 21 cat→read, head→read --max-lines, ls, tree, find, diff
Grep/Search 8 8 grep, rg → rtk grep
Network 5 5 curl, wget
Docker (supported) 18 18 ps, images, logs, run, build, exec, compose
Docker (blocked) 7 7 rm, rmi, stop, push, pull, network, volume
Kubectl (supported) 15 15 get, logs, describe, apply + global flags
Kubectl (blocked) 7 7 create, delete, edit, scale, exec, port-forward, rollout
JS/TS — vitest 9 9 vitest, npx vitest, pnpm vitest
JS/TS — tsc 11 11 tsc, npx tsc, pnpm tsc, vue-tsc, npx vue-tsc
JS/TS — eslint 9 8 pnpm eslint . not rewritten (see below)
JS/TS — prettier 5 5 prettier, npx prettier
JS/TS — playwright 5 5 playwright, npx playwright, pnpm playwright
JS/TS — prisma 7 7 prisma, npx prisma
NPM 6 6 npm test, npm run build/dev/lint/start
PNPM (supported) 3 3 list, ls, outdated
PNPM (blocked) 5 5 install, add, remove, run, dev
Python — pytest 8 8 pytest, python -m pytest + flags
Python — ruff (supported) 5 5 check, format
Python — ruff (blocked) 2 2 rule, linter
Python — pip/uv (supported) 8 8 list, outdated, install, show + uv variants
Python — pip/uv (blocked) 3 3 freeze, download
Go (supported) 9 9 test, build, vet + flags
Go — golangci-lint 3 3 run + flags
Go (blocked) 5 5 run, mod, install, get, generate
Env prefix 4 4 NODE_ENV=, RUST_LOG=, CI=, multi-var
Chained commands (&&) 4 4 git&&git, cargo&&cargo, cd&&ls, mkdir&&cd
Skip (no rewrite) 21 21 rtk *, echo, mkdir, cd, rm, mv, cp, chmod, which, env, export, source, python, node, make, cmake, gradle, mvn, dotnet
Heredoc 1 1 cat <<EOF skipped

Only issue: pnpm eslint . not rewritten

pnpm eslint . → no rewrite (expected rtk lint .).

The hook handles pnpm lint (script name) but not pnpm eslint (direct binary). Minor — most projects use pnpm lint in practice.

Notes on kubectl / docker compose syntax

The hook correctly mirrors real CLI syntax:

  • kubectl get podsrtk kubectl get pods
  • docker compose logs webrtk docker compose logs web

RTK currently uses shorter aliases (rtk kubectl pods, rtk docker ps/images/logs only), but that's a design issue on our side — we'll update RTK to accept the full natural syntax. No action needed on this PR.

Verdict

Excellent work. 312/313 commands rewrite correctly across 35 test categories. The chain splitting (&&, ;), env prefix preservation (NODE_ENV=test npm test), global flag stripping (git -C, --no-pager; docker -H; kubectl --namespace, -n), heredoc detection, and quote-aware parsing all work correctly.

The only fix needed is adding pnpm eslint support alongside the existing pnpm lint handling. Thanks!

I'm giving @aeppling the green light to merge once the pnpm eslint fix is in.

Split the monolithic 1127-line hook_cmd.rs into 8 focused modules under
src/hook/ for better maintainability and code review:

- mod.rs: entry point, chain parsing, orchestrator
- git.rs: git + GitHub CLI rewriters
- cargo.rs: cargo rewriter
- files.rs: file ops (cat, grep, ls, tree, find, diff, head) + network
- js_ts.rs: JS/TS tooling (vitest, tsc, eslint, prettier, playwright, prisma, pnpm)
- containers.rs: Docker + kubectl rewriters
- python.rs: pytest, ruff, pip/uv rewriters
- go.rs: go test/build/vet + golangci-lint rewriters
- helpers.rs: shared utility functions (replace_prefix, starts_with_any)

All 71 hook tests passing. No behavioral changes.
- Update version references from 0.18.1 to 0.19.0 in README.md,
  CLAUDE.md, and ARCHITECTURE.md
- Update module count in ARCHITECTURE.md (47 → 49)
- Update validate-docs workflow and script to check native hook module
  (src/hook/) instead of deprecated bash script (.claude/hooks/rtk-rewrite.sh)
@ayoub-khemissi
Copy link
Author

Fixes from review feedback

1. Split hook_cmd.rs into modules

The monolithic 1127-line file has been split into 9 files under src/hook/:

File Responsibility
mod.rs Entry point, chain parsing, orchestrator
git.rs Git + GitHub CLI
cargo.rs Cargo
files.rs cat, grep, ls, tree, find, diff, head, curl, wget
js_ts.rs vitest, tsc, eslint, prettier, playwright, prisma, pnpm
containers.rs Docker + kubectl
python.rs pytest, ruff, pip/uv
go.rs go test/build/vet, golangci-lint
helpers.rs Shared utilities (replace_prefix, starts_with_any)

All 71 hook tests preserved and passing.

2. Fix pnpm eslint (the 1/313 missing)

Added pnpm eslint .rtk lint . support in js_ts.rs, with 2 dedicated tests (test_pnpm_eslint, test_pnpm_eslint_bare).

313/313 commands now covered.

3. Fix CI validate-docs

  • Updated version 0.18.10.19.0 in README.md, CLAUDE.md, ARCHITECTURE.md
  • Fixed module count 4749 in ARCHITECTURE.md
  • Updated workflow and validate-docs script to check src/hook/ (native hook) instead of .claude/hooks/rtk-rewrite.sh (legacy bash hook)

- Take upstream version 0.20.1 for docs
- Update module count to 51 (upstream additions + hook split)
@pszymkowiak
Copy link
Collaborator

pszymkowiak commented Feb 20, 2026

Follow-up: Real Execution Testing + Migration Warning

Following up on my earlier 313-command protocol test — I went deeper and tested every RTK filter with real toolchains (not just the JSON rewrite protocol). Also found critical issues with the auto-migration behavior.

Real Execution: 98/98 pass

Built the PR branch (merged master, zero conflicts, 491 tests pass) and tested against actual projects in /tmp/:

Ecosystem Commands Pass Tools
Rust 9 9 cargo build/test/check/clippy/fmt
Node.js 15 15 vitest, eslint, prettier, playwright, tsc, pnpm, npm, prisma
Python 11 11 pytest, ruff check/format, pip list/outdated/show
Go 8 8 go test/build/vet, golangci-lint
Git 11 11 status, log, diff, branch, show, fetch, stash, etc.
GitHub CLI 9 9 pr, issue, run, release (list/view/diff)
Docker/K8s 4 4 ps, images, pods, services
Files 18 18 ls, tree, find, read, grep, diff, json, log, smart, wc, env, deps
Network 3 3 curl, wget
Meta 8 8 gain, version, init, err, summary, proxy
Total 98 98

All RTK filters work perfectly with real command output. Zero regressions vs master.


Critical: Don't Auto-Migrate to Native Hook

init.rs hardcodes HOOK_COMMAND = "rtk hook-rewrite" and migrate_old_hook() silently replaces the bash hook on rtk init.

This is breaking. Deeper testing (beyond the 313 protocol commands) revealed 3 P0 bugs that would silently corrupt user workflows:

P0 — Must fix before making native hook default

# Bug Example Impact
1 gh --json/--jq/--template rewritten gh pr list --json numberrtk gh pr list --json number Breaks JSON parsing for scripts/jq/Claude Code (#196)
2 Git global flags crash RTK git --no-pager logrtk git --no-pager log → error Breaks routinely used --no-pager
3 cat file1 file2 multi-file → crash cat f1.txt f2.txtrtk read f1.txt f2.txt → error rtk read only accepts single file

These aren't edge cases — gh --json and git --no-pager are used in almost every Claude Code session.

Suggested approach:

  1. Keep bash hook as default when merging
  2. Add opt-in: rtk init --native-hook for early adopters
  3. Fix P0s → then switch default in a future release

P1 — Should fix

# Bug Example Fix
4 head -n N not handled head -n 20 file → not rewritten Add regex for head -n (\d+) in hook/files.rs
5 Pipes not split git log | head -5 → rewritten as one command Split on | like &&/;

P2 — Nice to have (follow-up PRs)

pnpm install/pnpm run, env/printenv, bun commands, wc, tail -n N — all have RTK filters but no hook rewrite routing yet.


Roadmap: Toward an Extensible Hook Engine

This PR provides excellent rewrite coverage (hardcoded in Rust modules). Long term, we want the community to add rewrite rules without modifying Rust source code.

We plan to also leverage ideas from PR #156 (Hook Engine by @ahundt) which brings:

  • A lexer + chain analysis engine for quote-aware command parsing
  • A rtk run -c executor that routes commands to the right filter at runtime
  • A rtk hook check debugger for testing rewrites without execution

Combined with the declarative TOML plugin system (#139), this enables:

# ~/.config/rtk/plugins/terraform.toml
[filter]
command = "terraform"
subcommands = ["plan", "apply", "init"]
strategy = "strip"
patterns = ["^Acquiring", "^- Installed", "^Terraform has been"]

Community contributors would add support for new tools by dropping a .toml file — no Rust compilation needed.

Why not JS hooks? (ref: closed PR #141)

  • Performance: Node.js/Bun adds 30-80ms startup overhead per hook invocation — RTK targets <10ms. Every command triggers the hook; this latency compounds fast
  • Runtime dependency: Requires Node.js or Bun — breaks the single static binary philosophy
  • Maintenance burden: Duplicates regex logic across two codebases (bash + JS) that must stay in sync
  • Not extensible: Hardcoded patterns, no plugin mechanism
  • This PR's native Rust binary already solves Windows support (CI builds Windows binaries)

Bottom Line

RTK filters are rock solid (98/98 real execution). The hook architecture is clean and well-modularized.

Recommendation: Merge but keep bash hook as default. Fix the 3 P0s in follow-ups, then switch. Long term, combine with PR #156's engine + TOML plugins for community-extensible rewrite rules.

Happy to discuss any of these points.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P1-critical Bloque des utilisateurs, fix ASAP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants