Skip to content

feat(hooks): add cross-platform Node.js/Bun hook for Windows support#141

Closed
schizoidcock wants to merge 2 commits intortk-ai:masterfrom
schizoidcock:master
Closed

feat(hooks): add cross-platform Node.js/Bun hook for Windows support#141
schizoidcock wants to merge 2 commits intortk-ai:masterfrom
schizoidcock:master

Conversation

@schizoidcock
Copy link

What

  • Add rtk-rewrite.js - a cross-platform version of the hook that works on Windows, macOS, and Linux
  • Update rtk-awareness.md with installation instructions for both platforms

Why

The original rtk-rewrite.sh bash script doesn't work on Windows because:

  1. Windows can't execute .sh files directly
  2. Even with Git Bash, the path resolution and stdin handling differs

Key Features

  1. Cross-platform: Works with Node.js or Bun on any OS
  2. Chained command support: Handles cd "..." && git status patterns
    • Claude Code often generates commands like cd "/path" && git status
    • Extracts the last command after && for matching
    • Rewrites to: cd "/path" && rtk git status
  3. No external dependencies: Only requires Node.js or Bun (no jq needed on Windows)
  4. Same pattern matching: Equivalent regex logic to the bash version (grep/sed)

Platform Recommendations

  • macOS/Linux: Use rtk-rewrite.sh (bash + grep/sed + jq) - battle-tested
  • Windows: Use rtk-rewrite.js (Node.js/Bun) - native compatibility

Usage (Windows)

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "bun C:/Users/YOU/.claude/hooks/rtk-rewrite.js" }]
    }]
  }
}

## What
- Add `rtk-rewrite.js` - a cross-platform version of the hook that works on Windows, macOS, and Linux
- Update `rtk-awareness.md` with installation instructions for both platforms

## Why
The original `rtk-rewrite.sh` bash script doesn't work on Windows because:
1. Windows can't execute `.sh` files directly
2. Even with Git Bash, the path resolution and stdin handling differs

## Key Features
1. **Cross-platform**: Works with Node.js or Bun on any OS
2. **Chained command support**: Handles `cd "..." && git status` patterns
   - Claude Code often generates commands like `cd "/path" && git status`
   - Extracts the last command after `&&` for matching
   - Rewrites to: `cd "/path" && rtk git status`
3. **No external dependencies**: Only requires Node.js or Bun (no jq needed on Windows)
4. **Same pattern matching**: Equivalent regex logic to the bash version (grep/sed)

## Platform Recommendations
- **macOS/Linux**: Use `rtk-rewrite.sh` (bash + grep/sed + jq) - battle-tested
- **Windows**: Use `rtk-rewrite.js` (Node.js/Bun) - native compatibility

## Usage (Windows)
```json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{ "type": "command", "command": "bun C:/Users/YOU/.claude/hooks/rtk-rewrite.js" }]
    }]
  }
}
```

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@FlorianBruniaux
Copy link
Collaborator

Thanks for tackling Windows support — that's a real gap today.

A few points:

  1. Overlap with Gemini CLI support, rm→trash / git→stash data safety rtk.*.md rules, chained command rewriting, Rust-based hooks #131: @ahundt's PR also implements chained command rewriting (cd && git status patterns). Please coordinate to avoid duplicate work — ideally one approach wins and the other adapts
  2. CI is failing — could you check what's broken?
  3. Node.js/Bun as dependency: The current bash hook has zero external deps. Adding a Node.js/Bun requirement is a significant change for the install story. Is there a way to make it optional (Windows-only) while keeping the bash hook as default on Unix?

The cross-platform goal is valid, but let's make sure we don't regress the zero-dependency Unix experience.

@schizoidcock
Copy link
Author

schizoidcock commented Feb 16, 2026

Thanks for the detailed feedback, @FlorianBruniaux!

1. Re: Overlap with #131 (chained command rewriting)

I've reviewed #131 in detail. The key architectural difference:

Aspect PR #131 This PR (#141)
Hook logic location Rust binary (rtk hook claude) Standalone JS script
Shell script role 4-line shim → exec rtk hook claude Full logic (~200 lines)
Binary dependency Required - hook calls rtk binary Not required - hook is self-contained
Runtime requirement rtk binary installed Node.js, npm, or Bun
Chained commands Rust lexer in binary JS regex in hook
Gemini CLI support Yes (native) Yes (native) - same hook format (docs)

After #131 merges, the bash hook becomes a forwarding shim that requires the rtk binary. Users who want hook functionality must have the binary installed.

This PR provides: native Windows support for RTK hooks. Windows users can now use RTK natively — no path configuration issues, no WSL required, no compatibility problems.

Secondary benefits:

  • Works with runtimes most developers already have (Node.js, npm, or Bun)
  • Standalone hook - no rtk binary required
  • ~200 lines of auditable JS

Both approaches can coexist:

The chained command logic isn't duplicated effort — it's the same feature implemented for different deployment models.

2. Re: Node.js/Bun dependency concern

The bash hook remains the default on Unix/macOS/Linux. Nothing changes for existing users.

The JS hook is an opt-in alternative that:

  • Provides native Windows support for both Claude Code and Gemini CLI (bash scripts require WSL)
  • Uses runtimes that most developers already have installed (Node.js, npm, or Bun)
  • Zero additional dependencies for the majority of devs — no new installs needed

Install story comparison:

Platform Current (bash) With #131 With this PR (JS)
Linux/macOS bash + jq rtk binary Node.js, npm, or Bun (optional)
Windows Requires WSL rtk binary Node.js, npm, or Bun (native)

For most developers, Node.js/Bun is already installed — making this a lighter path than downloading a Rust binary or setting up WSL.

3. Re: CI status

CI is not failing — it's waiting for maintainer approval. The workflows triggered but show action_required:

Workflow Status
Security Check action_required
Documentation Validation action_required
Benchmark Token Savings action_required

This is standard GitHub Actions behavior for PRs from forks (security measure). A maintainer needs to approve the workflow runs — nothing broken on our side.

The changes only touch:

  • hooks/rtk-rewrite.js (new file)
  • hooks/rtk-awareness.md (documentation)

No Rust code, Cargo.toml, or core functionality affected.

Summary

This PR and #131 serve different user segments:

User type Recommended hook
Power users wanting safety rules #131 (Rust binary)
Windows users (Claude Code + Gemini CLI) This PR (JS hook, native)
Linux/macOS users preferring no binary This PR (JS hook, optional)

Proposal: Merge both. Document JS hook as "lightweight alternative" in README. Let users choose based on their needs.

@ahundt
Copy link

ahundt commented Feb 17, 2026

Hey @schizoidcock, thanks for this pr and your assessment!

The JS hook is well thought out, and you clearly dug into the details that matter (env var prefix handling, heredoc bypass, preserving all tool_input fields on rewrite).

There's significant overlap with PR #156 - Hook Engine + Chained Command Rewriting, which was split out from #131 per @FlorianBruniaux's feedback. It takes a different approach: the hook logic lives inside the rtk binary itself (rtk hook claude). Since Rust binaries are self-contained and RTK already ships Windows builds in CI, this gives native cross-platform support without requiring users to have Node.js or Bun installed. The config is just "command": "rtk hook claude" on every platform.

I went through your diff and compared it with PR #156, and the feature coverage lines up nicely. PR #156 also uses a lexer instead of regex for chained commands (so ||, ;, and quoted operators work too), includes tests for the hook protocol, and shares infrastructure with Gemini CLI support (PR #158).

I think having two separate hook implementations to keep in sync would be tricky over time, especially if a major dependency like js were to be added, so it's probably worth picking one path. Since the compiled binary approach doesn't introduce a Node.js or Bun dependency (which was @FlorianBruniaux's concern as well), I think it's a good fit.

That said, I'd really like to hear if you see gaps or scenarios where the binary approach falls short, for instance, some additional changes might be needed if additional shell languages need to be supported.

Replace regex-based chain parsing with proper lexer that:
- Handles &&, ||, ; operators (not just &&)
- Is quote-aware: "Fix && cleanup" won't incorrectly split
- Detects shellisms (globs, pipes, redirects, subshells)
- Rewrites each command in chain independently

Add 49 tests covering:
- Quote awareness edge cases
- Chain parsing with mixed operators
- Shellism detection and passthrough
- All supported command rewrites
- Env var prefix handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@schizoidcock
Copy link
Author

Thanks for the detailed feedback @ahundt!

I've updated the JS hook with a proper quote-aware lexer that now:

  • Handles &&, ||, and ; operators (not just &&)
  • Is quote-aware: git commit -m "Fix && cleanup" won't incorrectly split
  • Detects shellisms (globs, pipes, redirects, subshells) and passes through to shell
  • Rewrites each command in a chain independently
  • Includes 49 tests covering edge cases

Re: "Node.js as dependency" concern:

The target users of RTK hooks are AI coding tool users. Let's look at what they already have installed:

Tool Requires Node.js
Claude Code ✅ Yes
Gemini CLI ✅ Yes
opencode ✅ Yes
Most web dev workflows ✅ Yes

Node.js/Bun is effectively already a dependency for 95%+ of RTK's target audience. It's not adding a new dependency — it's leveraging one that's already there.

Key advantages of the JS hook approach:

Aspect Rust Binary JS Hook
Runtime Spawns new process Lightweight proxy (interpreter already warm)
Auditability Compiled binary ~350 lines of readable JS
Extensibility Rust + recompile + release Edit JS, test locally, done
Maintenance Binary releases per platform Single JS file, runs everywhere
Contribution barrier Rust knowledge required Any JS dev can contribute

The JS hook works as a proxy — it reads stdin, transforms, writes stdout. No process spawn overhead, no binary to download/update, no platform-specific builds.

Adding support for a new tool is trivial:

if (matchBinary === 'bun' && matchArgs[1] === 'test') {
  return `${envPrefix}rtk bun test ${argsToString(matchArgs.slice(2))}`.trim();
}

Proposal: Both approaches can coexist:

  • rtk hook claude (binary) → for users who want everything in the rtk binary
  • rtk-rewrite.js → lightweight, auditable, community-maintainable

Happy to discuss or close if the team prefers a single path.

@pszymkowiak
Copy link
Collaborator

thank for all the work here !

I think it's better for security and speed to go full binary "rtk hook". With a compiled Rust binary, we can implement binary signature verification — RTK would verify the hook's cryptographic signature (e.g. embedded ed25519 or codesigning) before execution, ensuring the hook hasn't been tampered with. This is something we simply can't enforce with a .js script that anyone can modify.

This gives us a tamper-proof trust chain that's impossible with an interpreted script approach. Happy to discuss implementation details.

we already receive a security PR in this way.

@schizoidcock
Copy link
Author

Thanks @pszymkowiak for the security perspective!

A few counterpoints to consider:

1. Threat model: The hook runs locally

If an attacker can modify rtk-rewrite.js on a user's machine, they already have filesystem access. At that point, they could also:

  • Replace the rtk binary itself
  • Modify ~/.claude/settings.json to point to a malicious hook
  • Inject code anywhere in the user's dev environment

Binary signature verification protects against distribution tampering, not local compromise.

2. Auditability cuts both ways

Aspect JS Hook Signed Binary
User can read the code ✅ Yes ❌ No
User can verify behavior ✅ Yes ❌ Trust the signature
Malicious code is visible ✅ Yes ❌ Hidden in binary

A signed binary means "trust RTK's build pipeline." A readable JS file means "trust what you can see." For security-conscious users, auditability > signatures.

3. Supply chain considerations

Binary signature verification requires:

  • Secure key management by RTK maintainers
  • Users trusting RTK's CI/CD pipeline
  • No compromise at any point in the release process

With JS, users can fork, audit, and run their own version. The attack surface is actually smaller because there's no trusted third party.

4. The real security risk

The hook receives commands from the AI and rewrites them. The security-critical question is: "Does the rewrite logic do what it claims?"

  • JS: Users can read 350 lines and verify
  • Binary: Users must trust the compiled output matches the source

That said, I understand the preference for a single implementation path. If the team decides to go binary-only, I'm happy to close this PR. Just wanted to present the counterarguments for consideration.

@aeppling aeppling added the P1-critical Bloque des utilisateurs, fix ASAP label Feb 18, 2026
@pszymkowiak
Copy link
Collaborator

Thanks for the thorough discussion and the quality of this PR — the lexer, the test suite, and the edge case handling are genuinely solid work.

You make a fair point about auditability vs signatures. For the local threat model you describe, you're right that a signed binary doesn't protect against someone who already has
filesystem access.

That said, we're going with the binary approach (rtk hook claude from PR #156) as the single path forward. The main reasons:

  1. Speed — Rust gives us sub-millisecond hook execution. The hook runs on every single Bash command Claude Code generates, so latency matters. A Node.js/Bun cold start adds 30-80ms
    overhead per invocation, which adds up fast in agentic loops with dozens of commands.
  2. One implementation to maintain — keeping bash + JS + binary hooks in sync as we add features (like the secrets vault we're working on, which adds a decrypt pass to the hook) would be a
    real maintenance burden
  3. Consistent architecture — everything in Rust, same build/test/release pipeline

Regarding your point about contribution barrier and extensibility — we agree that rewrite rules shouldn't require Rust knowledge to modify. The plan is to externalize the rule definitions
into TOML configuration, so users and contributors can add/customize rewrite patterns without touching Rust code or recompiling. The binary handles the lexer, chain parsing, and
execution; TOML handles the "what to rewrite" part.

If you'd like to explore that direction, we'd love a PR that designs the TOML schema for custom rewrite rules — something like:

[[rewrite]]
match = "terraform"
subcommands = ["plan", "apply", "init"]
rewrite = "rtk {command}"
???

That would let anyone add patterns for their stack without touching Rust. Your understanding of the rewrite logic from this PR puts you in a great position to design that. Happy to
discuss in GitHub Discussions or on Slack if you prefer.

please close in favor of #156, but genuinely appreciate the effort and hope to see you on another PR!

@pszymkowiak
Copy link
Collaborator

Thanks for the thorough discussion and the quality of this PR — the lexer, the test suite, and the edge case handling are genuinely solid work.

You make a fair point about auditability vs signatures. For the local threat model you describe, you're right that a signed binary doesn't protect against someone who already has filesystem access.

That said, we're going with the binary approach (rtk hook claude from PR #156) as the single path forward. The main reasons:

  1. Speed — Rust gives us sub-millisecond hook execution. The hook runs on every single Bash command Claude Code generates, so latency matters. A Node.js/Bun cold start adds 30-80ms overhead per invocation, which adds up fast in agentic loops with dozens of commands.
  2. One implementation to maintain — keeping bash + JS + binary hooks in sync as we add features (like the secrets vault we're working on, which adds a decrypt pass to the hook) would be a real maintenance burden
  3. Consistent architecture — everything in Rust, same build/test/release pipeline

Regarding your point about contribution barrier and extensibility — we agree that rewrite rules shouldn't require Rust knowledge to modify. The plan is to externalize the rule definitions into TOML configuration, so users and contributors can add/customize rewrite patterns without touching Rust code or recompiling. The binary handles the lexer, chain parsing, and execution; TOML handles the "what to rewrite" part.

If you'd like to explore that direction, we'd love a PR that designs the TOML schema for custom rewrite rules — something like:

[[rewrite]]
match = "terraform"
subcommands = ["plan", "apply", "init"]
rewrite = "rtk {command}"

That would let anyone add patterns for their stack without touching Rust. Your understanding of the rewrite logic from this PR puts you in a great position to design that. Happy to discuss in GitHub Discussions if you prefer.

Closing in favor of #156, but genuinely appreciate the effort and hope to see you on another PR!

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

Labels

awaiting-changes P1-critical Bloque des utilisateurs, fix ASAP

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

Comments