Skip to content

Gemini CLI support, rm→trash / git→stash data safety rtk.*.md rules, chained command rewriting, Rust-based hooks#131

Open
ahundt wants to merge 27 commits intortk-ai:masterfrom
ahundt:feat/multi-platform-hooks
Open

Gemini CLI support, rm→trash / git→stash data safety rtk.*.md rules, chained command rewriting, Rust-based hooks#131
ahundt wants to merge 27 commits intortk-ai:masterfrom
ahundt:feat/multi-platform-hooks

Conversation

@ahundt
Copy link

@ahundt ahundt commented Feb 15, 2026

PR131

Executive summary

Adds Gemini CLI support (rtk init --gemini), 11 data safety rules (rm→trash, git reset --hard→stash), chained command rewriting (cd && git status rewrites both), and extensible rtk.*.md data safety rule files. Closes #112 (chained commands missed) and #115 (destructive command safety). 53 files, 656 tests, 2 new deps (trash, which).

Features

# Feature What it does Impact
1 Gemini CLI support RTK now works on Gemini CLI — same token optimization, data safety rules, and rewrites as Claude Code. rtk init installs hooks for both platforms. First multi-platform support; 65% shared code, one new protocol handler per future platform
2 Data loss prevention (#115) 11 rules intercept destructive commands (rm, git reset --hard, git clean -f, etc.) and make them recoverable via trash or git stash Prevents ~20-50K token costs per data loss incident
3 Chained commands (#112) cd /path && git status now rewrites each command independently instead of only seeing cd Captures ~12-20M tokens/month in previously-missed optimizations
4 Extensible rtk.*.md data safety rule files Each rule is a markdown file with YAML frontmatter (patterns, action, redirect, when, env_var). 11 built-in rules ship compiled into the binary; users drop override/new files in .rtk/, .claude/, or .gemini/ dirs. Config system split into config/{mod,discovery,rules}.rs with [discovery] section, CRUD commands, and per-project overlay. User-customizable data safety rules and remaps without recompilation; same-name files override builtins

Context: RTK has saved 1.25B tokens across 3,757 tracked commands on my test machine. Issue #112 documents 271 git status, 65 tail, 81 grep, 105 ls calls in chained commands that were passing through unoptimized.


What Changed

1. Gemini CLI Support

RTK's token optimization now works on Gemini CLI in addition to Claude Code. Both platforms share the same command parsing, data safety rules, and rewrite engine. Only the JSON protocol layer differs.

  • rtk init with no flags installs both Claude and Gemini hooks
  • rtk init --claude installs Claude hooks only (~/.claude/settings.json)
  • rtk init --gemini installs Gemini hooks only (~/.gemini/settings.json)
  • Individual flags select one platform; combine --claude --gemini to install both, or use no flags for both
  • 65% code reuse between platforms — adding a third platform means writing one protocol handler

Architecture: src/cmd/hook.rs (shared logic) → src/cmd/claude_hook.rs (Claude JSON protocol) / src/cmd/gemini_hook.rs (Gemini JSON protocol)

2. Data Safety Rules (#115)

11 rule files (each an rtk.*.md file in src/rules/) intercept destructive commands and make them recoverable. The table below groups related rules into rows (e.g., git clean -f, -fd, -df are 3 separate rule files). Built-in rules use one pattern per file for clarity, though users can combine multiple patterns in a single rule file. Each incident of data loss costs ~20-50K tokens in recovery (investigate damage → restore/rewrite → regenerate → debug differences).

Pattern Rewrite
rm Move to OS trash instead of delete
git reset --hard git stash push -m 'RTK: reset backup' && git reset --hard {args}
git checkout . / git checkout -- git stash push -m 'RTK: checkout backup' && git checkout ... {args}
git clean -f/-fd/-df git stash -u -m 'RTK: clean backup' && git clean ... {args}
git stash drop git stash pop
cat, sed, head Block execution and return error with tool suggestion (use Read/Edit tools instead)

Action types: trash (move to OS trash via trash crate), rewrite (modify command before execution), warn (print message, allow execution), block/suggest_tool (fail with error and suggest alternative).

Rules define their own when: conditions. git reset --hard, git checkout ., and git checkout -- use when: has_unstaged_changes (only fire when there are uncommitted changes to protect). The remaining rules default to when: always. See src/rules/*.md for per-rule conditions and redirect templates.

Opt-out env vars: RTK_SAFE_COMMANDS=0 disables rm/git data safety rules. RTK_BLOCK_TOKEN_WASTE=0 disables cat/sed/head blocking. Each rule specifies which env var controls it via the env_var: field in its frontmatter.

Key files: src/cmd/safety.rs, src/rules/*.md

3. Chained Command Parsing (#112)

Before this PR, cd /path && git status && git diff only saw cd as the binary. git status and git diff passed through unoptimized. Each missed optimization wastes ~300K tokens/month at typical usage. The new lexer splits chains on &&, ||, ; (respecting quotes), rewrites each command independently, and reassembles the chain.

Before: cd /path && git status && git diff
  Hook sees "cd" → no rewrite for git status or git diff

After:  cd /path && git status && git diff
  → cd /path && rtk git status && rtk git diff
  Each command extracted and rewritten independently (95%/75% savings)
  • Smart quoting: git commit -m "Fix && Bug" doesn't split on && inside quotes
  • Pipes and redirects detected — commands like echo test | grep test execute as-is (no rewrites inside pipes)
  • Builtins (cd, export, pwd, echo) handled natively to preserve session state

Key files: src/cmd/lexer.rs, src/cmd/analysis.rs, src/cmd/builtins.rs

4. Extensible Data Safety Rule System

Rules are markdown files with YAML frontmatter (similar to SKILL.md files). 11 built-in data safety rules ship compiled into the binary via include_str!(). Users can override or extend them by placing rtk.*.md files in discoverable directories.

Built-in rulesrc/rules/rtk.safety.rm-to-trash.md:

---
name: rm-to-trash
patterns: [rm]
action: trash
redirect: "trash {args}"
env_var: RTK_SAFE_COMMANDS
---

Safety: Moving to trash instead of permanent deletion.

Built-in rulesrc/rules/rtk.safety.git-reset-hard.md:

---
name: git-reset-hard
patterns: ["git reset --hard"]
action: rewrite
redirect: "git stash push -m 'RTK: reset backup' && git reset --hard {args}"
when: has_unstaged_changes
env_var: RTK_SAFE_COMMANDS
---

Safety: Stashing before reset.

Users can create their own rules the same way. For example, intercepting chmod -R 777 only inside git repos (~/.config/rtk/rtk.safety.chmod-777.md):

---
name: chmod-777
patterns: ["chmod -R 777", "chmod 777"]
action: warn
when: "git rev-parse --is-inside-work-tree 2>/dev/null"
env_var: RTK_SAFE_COMMANDS
---

Warning: chmod 777 grants full read/write/execute to all users. Consider chmod 755 or chmod 644 instead.

The when: field supports named predicates (always, has_unstaged_changes) or arbitrary shell commands. Any command that exits 0 means the rule applies.

Rule priority (highest to lowest):

  1. --rules-add CLI paths
  2. .claude/, .gemini/, .rtk/ in each ancestor from cwd to $HOME (closest to cwd wins)
  3. ~/.claude/, ~/.gemini/ (global, LLM-visible)
  4. ~/.config/rtk/ (global config)
  5. Built-in include_str!() defaults

Same-name rules override by priority. enabled: false disables a rule entirely. All search directories are configurable via [discovery] in config.toml.

Key files: src/config/rules.rs, src/config/discovery.rs

Config changes: Split config.rs into config/{mod,discovery,rules}.rs. Added [discovery] section for configurable search dirs, global dirs, and rules dirs. Config CRUD via rtk config get/set/unset/list. Per-project .rtk/config.toml overlay. Env var overrides: RTK_SEARCH_DIRS and RTK_RULES_DIRS (comma-separated paths, replace defaults when set).


Implementation Notes

  1. Hooks moved from shell to Rust
    • Previous: hooks/rtk-rewrite.sh (204 lines of hard-coded sed/grep patterns, only rewrote the first command in chains)
    • New: rtk hook claude / rtk hook gemini (native Rust handlers)
    • Improvements over the shell approach:
      1. Chained commands work. cd /path && git status rewrites both commands independently (core fix for Hook: chained commands (cd dir && cmd) are never rewritten #112)
      2. Extensible. Add new commands via src/rules/*.md files, not bash edits
      3. Debuggable. rtk hook check <cmd> shows exact rewrites (impossible with bash)
      4. Multi-platform. Single codebase for both Claude Code and Gemini CLI (protocol adapters only)
      5. Backward compatible. Legacy hook script becomes a 3-line shim that forwards to rtk hook claude
  2. Backward compatible: All existing token-saving features work unchanged. Data safety rules are opt-out via env vars.
  3. Binary size: 5.1 MB (+0.3 MB over master's 4.8 MB, from trash + which deps and embedded rules; LTO + stripping enabled).
  4. New dependencies: trash (cross-platform trash API for rm safety), which (binary path validation for hook installation).

Not Included (Future)

  • Extended data safety rules: sudo rm, docker rm, kubectl delete with confirmations
  • Analytics on which data safety rules fire most often
  • Additional built-in predicates (currently: always, has_unstaged_changes, plus arbitrary shell commands)

Review Guide

  1. src/cmd/lexer.rs + src/cmd/analysis.rs — Chain parsing correctness
  2. src/cmd/safety.rs + src/rules/ — Data safety rule matching and rewriting
  3. src/cmd/claude_hook.rs + src/cmd/gemini_hook.rs — Protocol compliance with each platform
  4. src/config/rules.rs + src/config/discovery.rs — Rule loading, precedence, override semantics
  5. src/init.rs — Installation flow (rtk init --claude --gemini)
  6. Cargo.toml — New dependencies (trash, which) validated through functional tests above

Test Plan

  • cargo test — 656 tests pass
  • rtk hook check "rm file" — hook rewrites to rtk run -c 'rm file' (hook layer)
  • rtk run -c "rm file" — executes the rewrite: file moved to trash, not deleted (execution layer)
  • rtk hook check "cd /tmp && git status" — rewrites both commands independently
  • RTK_SAFE_COMMANDS=0 rtk run -c "rm file" — passes through (opt-out works)
  • RTK_BLOCK_TOKEN_WASTE=0 rtk run -c "cat file" — cat executes (opt-out works)
  • In a clean working tree, rtk run -c "git reset --hard" — no stash created (when: has_unstaged_changes condition fails)
  • rtk run -c "git clean -fd" — stashes untracked files before cleaning (when: always)
  • rtk run -c "git stash drop" — rewrites to git stash pop (when: always)
  • rtk config list — shows [discovery] section with search_dirs, global_dirs
  • rtk config set discovery.search_dirs .rtk,.custom && rtk config get discovery.search_dirs — config CRUD works
  • rtk init --claude && rtk init --gemini — installs hooks for both platforms
  • Place custom rtk.safety.chmod-777.md in ~/.config/rtk/rtk run -c "chmod 777 file" triggers warning
  • Place override rtk.safety.rm-to-trash.md with enabled: false in .rtk/ — project-level rule overrides global, rm passes through

Please let me know what you think!

Implements a "Fat Binary, Thin Hook" architecture that enables RTK to
safely execute arbitrary commands while applying token optimization
and safety rules.

Core components:
- src/cmd/lexer.rs: Quote-aware state-machine lexer that correctly
  handles operators (&&, ||, ;) inside quoted strings
- src/cmd/analysis.rs: Native vs passthrough decision engine
- src/cmd/safety.rs: Safety rules with env var toggles (RTK_SAFE_RM,
  RTK_SAFE_GIT) and tool suggestions (cat->Read, sed->Edit)
- src/cmd/trash_cmd.rs: Built-in cross-platform trash using `trash` crate
- src/cmd/builtins.rs: Native cd/export/pwd/echo implementations
- src/cmd/exec.rs: Hybrid executor with recursion guard (RTK_ACTIVE)
- src/cmd/filters.rs: Registry connecting binaries to token reducers
- src/cmd/hook.rs: Claude text protocol handler
- src/cmd/gemini_hook.rs: Gemini JSON protocol handler

New CLI commands:
- `rtk run -c <cmd>`: Execute command through hybrid engine
- `rtk hook check --agent <claude|gemini> <cmd>`: Hook protocol check
- `rtk hook gemini`: Handle Gemini JSON stdin

Features:
- Handles chained commands (cd dir && git status)
- Short-circuit evaluation (&&, ||, ;)
- Passthrough for shellisms (*, $, pipes, redirects)
- Safety: rm -> trash, git clean -fd -> block, cat -> suggest Read

Tests: 312 new tests in cmd module, all 499 total tests passing

Closes rtk-ai#112 (chained commands not rewritten)
Refs rtk-ai#115 (safe command remapping)
Hook script (hooks/rtk-rewrite.sh):
- Add RTK_HOOK_ENABLED master toggle (default: 1)
- Add RTK_HOOK_HYBRID to use rtk hook check (default: 1)
- Add RTK_ACTIVE recursion guard
- Delegate to rtk hook check for intelligent command analysis
- Output proper deny response when commands are blocked

Safety module (src/cmd/safety.rs):
- Consolidate env vars to coarse-grained toggles:
  - RTK_SAFE_COMMANDS=1 enables all safety features (rm->trash, git safety)
  - RTK_BLOCK_TOKEN_WASTE=0 disables token waste prevention
- Token waste prevention (cat/sed/head) is enabled by default
- Update all tests for new env var names

Builtins (src/cmd/builtins.rs):
- Fix flaky cd test assertion

Environment Variables Summary:
- RTK_HOOK_ENABLED=0|1       - Master hook toggle (default: 1)
- RTK_HOOK_HYBRID=0|1        - Use hybrid engine (default: 1)
- RTK_SAFE_COMMANDS=1        - Enable command safety (rm->trash, git)
- RTK_BLOCK_TOKEN_WASTE=0    - Disable cat/sed/head blocking
- RTK_ACTIVE=1               - Recursion guard (internal)
The Claude Code hook specification requires exit code 2 for blocking
errors, not exit code 1. Using exit 1 may be treated as non-blocking,
potentially allowing blocked commands to proceed.

Changes:
- hook.rs: format_for_claude() now returns exit code 2 for Blocked
- rtk-rewrite.sh: Changed exit 1 to exit 2 for deny responses
- Added 13 new edge case tests for hook protocol coverage:
  - head command blocking
  - env var prefix handling
  - backtick/subshell/brace expansion shellisms
  - chained command handling
  - special characters and unicode
  - very long commands
  - exit code verification tests
  - multi-agent support verification

All 572 tests pass.
Verify that cat/sed/head blocking is context-aware:
- cat with pipe (cat file | grep x) → allowed via passthrough
- cat with redirect (cat file > out) → allowed via passthrough
- sed with redirect (sed s/x/y/ f > out) → allowed via passthrough
- head with pipe (head -n 10 f | grep) → allowed via passthrough
- cat standalone → blocked (should use Read tool)
- cat in chain (cd dir && cat file) → blocked (should use cd + Read)
- cat in complex script (for loops, etc.) → allowed via passthrough

This ensures token waste prevention only blocks commands that
could be replaced with proper tools, not legitimate shell usage.

All 579 tests pass.
Parallel test execution caused intermittent failures when tests
accessed global environment variables. Added static OnceLock<Mutex>
to serialize tests that modify RTK_SAFE_COMMANDS and RTK_BLOCK_TOKEN_WASTE.

Changes:
- safety.rs: Added ENV_LOCK mutex, all 20 tests use env_lock()
- edge_cases.rs: Added same pattern for 9 safety edge case tests

The mutex ensures only one test modifies env vars at a time,
preventing race conditions while keeping parallel execution for
other tests that dont touch env vars.

All 579 tests pass reliably with parallel execution.
Changed RTK_SAFE_COMMANDS from opt-in to opt-out:
- rm -> trash: now enabled by default
- git reset --hard -> stash prepend: now enabled by default
- git clean -fd/-df -> block: now enabled by default

To disable safety features, set RTK_SAFE_COMMANDS=0

Also fixed:
- check_raw() function to use opt-out logic
- Mutex poison recovery in test env_lock()
- Updated all affected tests

All 582 tests pass.
- Silent on success (like rm behavior)
- Single-line error with bypass hint
- Removed verbose progress messages
- Reduced from 5+ lines to 0-1 lines output

Before: 'Moving X item(s) to trash...\nDone.\n'
After: (silent)

Before: 6-line error with suggestions
After: 'trash: ✗ <error> (RTK_SAFE_COMMANDS=0 to bypass)'
The SuggestTool action appends 'Use the **X** tool.' so agent_msg
should not include the tool suggestion to avoid duplication.

Before: 'BLOCK: cat wastes tokens. Use Read tool.. Use the **Read** tool.'
After: 'BLOCK: cat wastes tokens. Use the **Read** tool.'
- Empty args: 'rm: missing operand'
- Missing files: 'rm: cannot remove X: No such file'
- Silent on success (like rm)
- Concise error on trash failure
- git checkout . -> prepend stash (with predicate: has_unstaged_changes)
- git checkout -- -> prepend stash (with predicate)
- git stash drop -> rewrite to stash pop (recoverable)
- git clean -f -> block (suggest -n dry-run)
- git checkout <branch> still works (not matched)
- git checkout -b still works (not matched)

All rules use RTK_SAFE_COMMANDS env var, enabled by default.
… init

Previous behavior:
- safety.rs: starts_with(pattern) matched "catalog" as "cat", "sedan" as "sed"
- exec.rs: RTK_ACTIVE env var leaked if execute_inner() panicked
- hook.rs: no recursion depth limit on safety rewrite chains
- gemini_hook.rs: used wrong field names ("type", "result", "message",
  "modified_input") that Gemini CLI silently ignored
- filters.rs: duplicate strip_ansi() reimplemented vs utils::strip_ansi()
- init.rs: no Gemini CLI hook setup support
- predicates.rs: used deprecated atty crate

What changed:

Bug fixes:
- safety.rs: match single-word patterns against binary exactly, multi-word
  against full_cmd.starts_with(); fix check_raw() word boundary matching
- exec.rs: replace set/unset_rtk_active() with RAII RtkActiveGuard (Drop cleans
  up even on panic)
- hook.rs: add MAX_REWRITE_DEPTH=3 with check_for_hook_inner() recursion limit
- gemini_hook.rs: rewrite to match actual Gemini CLI API spec -- use
  hook_event_name, decision, reason, hookSpecificOutput.tool_input; add
  is_shell_tool() filter for run_shell_command/shell/MCP patterns
- filters.rs: delete duplicate strip_ansi(), use crate::utils::strip_ansi;
  deduplicate apply() by delegating to apply_to_string()
- predicates.rs: replace atty::is() with std::io::IsTerminal, remove atty dep
- builtins.rs: fix echo -n flag handling (was printing "-n" literally)

DRY refactoring (-735 net lines):
- safety.rs: add rule!() declarative macro for SafetyRule construction
- hook.rs: table-driven tests with assert_rewrite()/assert_blocked() helpers
- edge_cases.rs: prune from 660 to 122 lines, keep only cross-module
  integration tests (unit tests live in their home modules)
- test_helpers.rs: new shared EnvGuard with RAII cleanup for env-mutating tests
- filters.rs: document as fallback filter (20-40%) vs dedicated modules (60-90%)

Dead code removal:
- lexer.rs: remove has_shellisms(), merge identical if blocks
- analysis.rs: remove ExecutionPlan enum and analyze() function
- predicates.rs: remove 7 unused functions (has_staged_changes, stash_exists,
  is_file, is_dir, path_exists, in_git_repo, binary_exists)
- safety.rs: remove unused SafetyAction::Allow and SafetyAction::Block variants
- hook.rs: remove unused format_for_gemini()
- trash_cmd.rs: remove unused is_available()
- ruff_cmd.rs: remove unused RuffLocation struct and unused fields

New feature:
- init.rs: add rtk init --gemini for Gemini CLI hook setup -- patches
  ~/.gemini/settings.json with BeforeTool hook, supports --auto-patch,
  --no-patch, and --uninstall; parallel to existing Claude Code init
- main.rs: route --gemini flag to init::run_gemini()

Tests added (546 total, 0 failures):
- gemini_hook.rs: 17 protocol conformance tests (field names, tool filtering,
  event filtering, edge cases)
- hook.rs: 9 Claude Code wire format tests (exit codes, text output) +
  cross-protocol consistency tests
- init.rs: 11 Gemini init tests (idempotency, deep-merge, independence from
  Claude hooks)
- exec.rs: RAII guard panic-safety test via catch_unwind

Files affected:
- src/cmd/safety.rs: pattern matching fix, rule!() macro, remove dead variants
- src/cmd/exec.rs: RtkActiveGuard RAII, flatten if-else
- src/cmd/hook.rs: recursion limit, table-driven tests, remove dead code
- src/cmd/gemini_hook.rs: complete rewrite to match Gemini CLI API spec
- src/cmd/filters.rs: dedup strip_ansi, dedup apply(), add module docs
- src/cmd/edge_cases.rs: prune to integration-only tests
- src/cmd/test_helpers.rs: new shared EnvGuard
- src/cmd/lexer.rs: remove dead code, merge branches
- src/cmd/analysis.rs: remove dead code
- src/cmd/predicates.rs: remove dead code, replace atty
- src/cmd/builtins.rs: fix echo -n, clean test assertions
- src/cmd/trash_cmd.rs: remove dead code
- src/cmd/mod.rs: register test_helpers module
- src/init.rs: add Gemini CLI init support (9 new functions, 11 tests)
- src/main.rs: add --gemini flag, route to init::run_gemini()
- src/ruff_cmd.rs: remove unused structs/fields
- Cargo.toml: remove atty dependency
- Various *.rs: minor clippy fixes (container, find_cmd, gh_cmd, git, etc.)

Testable:
- cargo test (546 pass)
- cargo run -- hook check "catalog list" (should NOT trigger cat block)
- cargo run -- init --gemini --auto-patch (creates ~/.gemini/settings.json)
…tension manifest

Previous behavior:
- README.md: no documentation for Gemini CLI integration
- init.rs: show_config() only checked Claude Code artifacts (hook, RTK.md,
  CLAUDE.md, settings.json) — no Gemini status
- No gemini-extension.json for `gemini extensions install`

What changed:
- README.md: add "Gemini CLI Integration" section with quick install
  (`rtk init --gemini`), manual install (settings.json snippet), protocol
  explanation (BeforeTool → rtk hook gemini), and uninstall instructions;
  update "Uninstalling RTK" to mention ~/.gemini/settings.json removal
- src/init.rs: show_config() now calls resolve_gemini_dir() and
  gemini_hook_already_present() to check ~/.gemini/settings.json for the
  RTK BeforeTool hook entry; displays status with same checkmark/warning/circle
  pattern as Claude Code checks; adds three Gemini usage lines to help output
- gemini-extension.json: new manifest file for `gemini extensions install`
  with name, version, description, entryPoint, hooks config, repo URL, license

Files affected:
- README.md: +62 lines (Gemini CLI Integration section, uninstall update)
- src/init.rs: +34 lines (Gemini status in show_config, usage lines)
- gemini-extension.json: new file (extension manifest)

Testable:
- cargo test (546 pass, 0 fail)
- cargo run -- init --show (should display Gemini settings.json status)
…eanup

Previous behavior:
- exec.rs: spawn_with_filter() used spawn() + piped reads before wait(),
  deadlocking if child output exceeded OS pipe buffer (~64KB)
- exec.rs: rtk run -c "git status" flattening took args[1] ("-c") instead
  of args[2] (the actual command)
- exec.rs: timer.track() passed filtered output as both raw and filtered,
  making savings always 0%
- exec.rs: run_passthrough() merged stderr into stdout via single print!()
- exec.rs: RAII guard tests called set_var/remove_var without EnvGuard
  mutex, racing with other env-mutating tests
- lexer.rs: single & (background job) classified as Redirect, not Shellism;
  ! (history expansion/negation) not recognized as shellism
- safety.rs: check_raw() sudo detection used windows(2) — "sudo -u root rm"
  bypassed because rm is not adjacent to sudo
- safety.rs: Rewrite action called template.replace("{args}", ...) but no
  rule uses {args} — dead code path
- safety.rs: block messages said "Use Read tool" / "Use Edit tool" which
  are Claude Code tool names, not understood by Gemini CLI
- gemini_hook.rs: dead branch (new_cmd == cmd) never triggered because
  check_for_hook wraps all safe commands in rtk run -c
- gemini_hook.rs: _tool_name cloned a string that was never used
- hook.rs: non-exhaustive _ => {} match silently swallowed future variants
- filters.rs: apply() function left unused after spawn → output() refactor

What changed:
- exec.rs: replace spawn() + piped read with Command::output() (uses
  internal threads, no deadlock); fix -c flag handling in rtk run
  flattening; separate raw vs filtered for accurate tracking; print
  stderr to stderr in passthrough; wrap RAII tests with EnvGuard
- lexer.rs: classify single & as Shellism (background jobs need real
  shell); add ! to shellism characters
- safety.rs: scan all words after sudo for rm (not just adjacent);
  remove dead {args} template substitution; make block messages
  agent-generic ("file-reading tool" instead of "Read tool")
- gemini_hook.rs: remove dead new_cmd == cmd branch; remove unused
  _tool_name clone
- hook.rs: replace _ => {} with explicit Rewritten/TrashRequested arms
- filters.rs: remove unused apply() function and ChildStdout/Stderr imports
- cargo fmt applied to all src/cmd/ files (92 formatting fixes)

Files affected:
- src/cmd/exec.rs: deadlock fix, -c flattening, tracking, stderr, EnvGuard
- src/cmd/safety.rs: sudo scan, agent-generic messages, remove {args}
- src/cmd/lexer.rs: & as Shellism, ! as shellism
- src/cmd/hook.rs: exhaustive match arms
- src/cmd/gemini_hook.rs: remove dead branch and unused clone
- src/cmd/filters.rs: remove dead apply() function
- src/cmd/analysis.rs, builtins.rs, mod.rs, predicates.rs, trash_cmd.rs:
  cargo fmt only
- src/container.rs, git.rs, init.rs, main.rs, pip_cmd.rs, utils.rs:
  cargo fmt only

Testable:
- cargo test (549 pass, 0 fail)
- cargo clippy --all-targets (0 warnings in src/cmd/)
…en visibility

Previous behavior: The src/cmd/ module used vague terms like "hybrid command
engine", "hybrid", and "native" throughout docs, comments, env vars, and
scripts. init.rs duplicated ~100 lines of identical JSON read/check/backup/write
logic across Claude and Gemini hook setup paths. Seven internal modules were
pub instead of pub(crate). edge_cases.rs contained 16 tests, 9 of which
duplicated tests already in exec.rs.

What changed:
- README.md: "hybrid engine" → "safety checks and token-optimized output"
- hooks/rtk-rewrite.sh: RTK_HOOK_HYBRID → RTK_HOOK_REWRITE, all "hybrid
  engine"/"native mode" refs replaced with "rewrite mode"/"safety rewrite"
- scripts/test-hybrid-engine.sh → scripts/test-cmd-interceptor.sh (renamed)
- gemini-extension.json: deleted (dead metadata, no code references it;
  Gemini integration works via rtk init --gemini → ~/.gemini/settings.json)
- src/cmd/mod.rs: doc rename, 7 pub mod → pub(crate) mod (analysis,
  builtins, filters, lexer, predicates, safety, trash_cmd), removed
  mod edge_cases declaration
- src/cmd/predicates.rs: 4 pub fn → pub(crate) fn (has_unstaged_changes,
  is_interactive, expand_tilde, get_home)
- src/cmd/exec.rs: doc rename, added 9 tests (7 moved from edge_cases.rs
  + 2 restoring coverage for 3-command && chain and semicolon-last-wins)
- src/cmd/edge_cases.rs: deleted (9 duplicate tests dropped, 7 unique
  tests moved to exec.rs)
- src/cmd/hook.rs: merged test_chain_rewrite and test_very_long_command
  into test_safe_commands_rewrite table, preserved && operator assertion
- src/cmd/filters.rs: doc "hybrid engine" → "rtk run"
- src/main.rs: doc "hybrid engine" → "safety checks and token-optimized output"
- src/init.rs: extracted 3 shared functions replacing 6 copy-pasted ones:
  patch_settings_shared() for JSON hook patching (Claude + Gemini),
  remove_hook_from_settings_file() for hook removal (Claude + Gemini),
  show_agent_hook_status() for config display (Claude + Gemini)

Why: Vague terminology made the codebase harder to understand. Duplicated
init.rs patterns meant adding a third agent hook would require copy-pasting
~80 lines. Public internal modules allowed misuse from outside src/cmd/.

Files affected: 12 files, -373/+231 lines (net -142). 540 tests pass.
…ude`

Previous behavior: Claude Code hook used a 257-line bash script
(hooks/rtk-rewrite.sh) that required jq, duplicated safety logic
with regex fallbacks, and couldn't maintain state across invocations.
Gemini hook was already a direct binary call (`rtk hook gemini`).

What changed:
- src/cmd/claude_hook.rs: New 453-line module (24 tests) implementing
  Claude Code JSON hook protocol with fail-open run()/run_inner() split.
  Serde structs with camelCase field names match Claude Code spec exactly
  (hookSpecificOutput, permissionDecision, updatedInput).
- src/cmd/mod.rs: Register claude_hook module
- src/main.rs: Add `Claude` variant to HookCommands enum with routing
- src/init.rs: Remove dead code (REWRITE_HOOK, prepare_hook_paths,
  ensure_hook_installed ~80 lines). Change patch_settings_json() to
  register "rtk hook claude" directly. Update hook_already_present()
  and remove_hook_from_json() to match both legacy rtk-rewrite.sh
  and new rtk hook claude patterns. Update show_config() to check
  settings.json instead of hook file existence.
- src/cmd/hook.rs: Add 3 new test functions porting coverage from
  deleted shell tests: env var prefix preservation (7 commands),
  specific command pass-through (34 commands including npm/docker/
  kubectl/vitest/vue-tsc), and builtin pass-through (8 commands).
- hooks/rtk-rewrite.sh: 257 lines -> 4-line migration shim
  (`exec rtk hook claude`)
- hooks/test-rtk-rewrite.sh: Deleted (293 lines). All coverage
  now in Rust unit tests.

Why: Achieves architectural parity with Gemini hook. Eliminates jq
dependency. Reduces hook latency from ~50-150ms (bash+jq) to ~1-2ms
(Rust). Unlocks future statefulness (session-scoped rules, persistent
cd tracking via existing tracking.rs SQLite infrastructure). Net
reduction of ~91 lines (611 added, 702 removed).

Files affected:
- src/cmd/claude_hook.rs (new): Protocol handler + 24 tests
- src/cmd/hook.rs: +94 lines (3 new test functions, 49 test cases)
- src/cmd/mod.rs: +1 line (module registration)
- src/main.rs: +5 lines (enum variant + match arm)
- src/init.rs: -209 lines net (dead code removal + updates)
- hooks/rtk-rewrite.sh: -254 lines (257 -> 4 line shim)
- hooks/test-rtk-rewrite.sh: -293 lines (deleted)

Testable: 566 tests pass (24 new in claude_hook, 3 new in hook.rs)
  echo '{"tool_input":{"command":"git status"}}' | cargo run -- hook claude
  echo '{"tool_input":{"command":"cat /etc/passwd"}}' | cargo run -- hook claude
  echo 'bad json' | cargo run -- hook claude  # exit 0, no output
…k claude

Previous behavior: README.md, INSTALL.md, TROUBLESHOOTING.md, and
check-installation.sh still referenced the old hooks/rtk-rewrite.sh
shell script path for hook installation, manual setup, and uninstall.

What changed:
- README.md: Update 7 references to use rtk hook claude
- INSTALL.md: Update 2 references for settings.json registration
- docs/TROUBLESHOOTING.md: Rewrite manual fallback to use rtk hook claude
- scripts/check-installation.sh: Replace file check with settings.json check
- src/init.rs: Add test_remove_hook_from_json_new_format test

Why: Previous commit (883924b) consolidated hook logic into Rust binary
but did not update documentation and scripts that still directed users
to copy shell scripts and referenced paths no longer used.

Files: README.md, INSTALL.md, docs/TROUBLESHOOTING.md,
scripts/check-installation.sh, src/init.rs (+1 test, 567 total)
…ni parity

Previous behavior:
- Both hook modules used println!/eprintln! directly, risking JSON corruption
- Guard functions (is_disabled, should_passthrough) duplicated in claude_hook.rs
- Gemini hook lacked fail-open wrapper (run/run_inner split)
- Gemini hook lacked recursion and disabled checks
- Claude hook had 3 integration tests duplicating hook.rs tests
- Gemini CLI setup not documented

What changed:
- hook.rs: Added shared guards (is_hook_disabled, should_passthrough) and HookResponse enum
- Both hooks: Added #![deny(clippy::print_stdout, clippy::print_stderr)] for compile-time enforcement
- Both hooks: Refactored run_inner() to return HookResponse (no I/O), run() is single I/O point
- claude_hook.rs: Removed local guard functions, import from hook.rs (DRY)
- gemini_hook.rs: Added missing fail-open wrapper and guard checks
- claude_hook.rs: Removed 3 duplicate integration tests (now only in hook.rs)
- INSTALL.md: Added Gemini CLI Setup section with rtk init --gemini
- TROUBLESHOOTING.md: Added "RTK not working in Gemini CLI" section
- check-installation.sh: Added Check 7 for Gemini hook status

Why:
- Compile-time I/O enforcement prevents accidental protocol corruption
- DRY eliminates 22 lines of duplicated guard logic
- Separation of concerns: logic in run_inner(), I/O in run()
- Gemini hook now has same robustness guarantees as Claude hook
- Documentation supports both Claude Code and Gemini CLI users equally

Files:
- src/cmd/hook.rs: +37 lines (shared infrastructure)
- src/cmd/claude_hook.rs: refactored, -3 tests, +doc comments
- src/cmd/gemini_hook.rs: +fail-open wrapper, +guards, +2 tests
- INSTALL.md: +31 lines (Gemini setup)
- docs/TROUBLESHOOTING.md: +52 lines (Gemini troubleshooting)
- scripts/check-installation.sh: +8 lines (Gemini verification)

Testable:
- cargo test — all 567 tests pass (-3 from deduplication)
- cargo clippy — no print_stdout/print_stderr violations
- echo '{"tool_input":{"command":"git status"}}' | rtk hook claude
- echo '{"hook_event_name":"BeforeTool","tool_name":"run_shell_command","tool_input":{"command":"git status"}}' | rtk hook gemini
…ni uninstall

Previous behavior:
- Module doc comments did not explain WHY deny attribute is needed or WHERE it applies
- No reference to API spec rule "ANY stderr at exit 0 = hook error"
- tool_input command replacement duplicated in both hook modules (6-7 lines each)
- Gemini hook lacked dedicated uninstall function (only removed via Claude uninstall)
- No inline comments explaining bug #4669 dual-path deny at I/O points
- No architecture diagram showing where clippy deny is enforced

What changed:
- claude_hook.rs: Added module doc with stderr rule (lines 13-23), bug #4669 details (lines 25-37), I/O scope (lines 39-51), API spec citations (line 13), enhanced run() comments with box diagram (lines 172-182)
- gemini_hook.rs: Added module doc with Gemini stderr rule (lines 13-22), I/O scope (lines 24-39), enhanced run() comments with box diagram (lines 115-125)
- hook.rs: Added architecture diagram (lines 6-15), I/O policy scope (lines 17-25), pathway showing deny enforcement (line 29), extracted update_command_in_tool_input() helper (lines 138-158)
- init.rs: Added uninstall_gemini() function (lines 429-452) removing hook from ~/.gemini/settings.json
- main.rs: Route --gemini --uninstall to init::uninstall_gemini() (lines 1079-1083)
- Both hooks: Import and use shared update_command_in_tool_input() (replaced 6-7 line blocks with 1 call)

Why:
- API spec compliance: Document exact stderr rules per hooks_api_reference.md:720-728 (Claude) and 740-753 (Gemini)
- Clarity: Developers need to know WHERE restrictions apply (2 files) vs normal behavior (all others)
- Bug #4669 transparency: Explain dual-path deny workaround inline at I/O points
- DRY: Eliminate 11 lines of duplicated tool_input preservation logic
- Parity: Gemini now has equal uninstall support (rtk init --gemini --uninstall)
- Maintainability: Source citations enable verification against official docs

Files:
- src/cmd/claude_hook.rs: +52 doc lines, -6 code (replaced with shared call)
- src/cmd/gemini_hook.rs: +44 doc lines, -5 code (replaced with shared call)
- src/cmd/hook.rs: +38 doc lines, +20 code (new shared helper + docs)
- src/init.rs: +24 lines (new uninstall_gemini function)
- src/main.rs: +4 lines (route Gemini uninstall)

Testable:
- cargo test — all 567 tests pass (no behavior changes)
- cargo clippy — no violations
- rtk init --gemini --uninstall — now works (new functionality)
- Verify doc accuracy: compare src/cmd/claude_hook.rs:13-23 against hooks_api_reference.md:720-728
… flags

Previous behavior:
- rtk init with no flags → Claude Code only
- rtk init --gemini → Gemini CLI only (mutually exclusive)
- No way to set up both platforms in one command
- Uninstall was platform-exclusive (either/or)

What changed:
- Init command description: "Initialize rtk for Claude Code and/or Gemini CLI (default: both)"
- Moved --gemini out of mode group, into new "platform" group
- Added --skip-claude flag (platform group): Skip Claude Code setup
- Added --skip-gemini flag (platform group): Skip Gemini CLI setup
- Updated routing logic (main.rs:1089-1117):
  - setup_claude = \!skip_claude && \!gemini (default: true)
  - setup_gemini = \!skip_gemini || gemini (default: true)
  - Error if both platforms skipped
  - Run both if both enabled (sequential execution)
  - Summary message when both platforms set up
- Updated uninstall logic (main.rs:1088-1101): Same platform selection logic

Why:
- DX improvement: One command sets up both CLIs (most users have both)
- Backward compatibility: --gemini still works (alias for --skip-claude)
- Selective setup: Can choose one platform if needed (--skip-claude or --skip-gemini)
- Consistent behavior: Install and uninstall use same platform selection logic

Usage examples:
- rtk init -g                    → Both Claude and Gemini (new default)
- rtk init --skip-gemini         → Claude only
- rtk init --skip-claude         → Gemini only
- rtk init --gemini              → Gemini only (backward compat)
- rtk init --uninstall           → Remove both
- rtk init --uninstall --skip-claude → Remove Gemini only

Testable:
- cargo test — all 567 tests pass
- rtk init --help — shows new platform flags
- Platform selection error: rtk init --skip-claude --skip-gemini (should error)
…laude/--gemini

Previous behavior:
- No documentation of which init.rs functions are shared vs platform-specific
- Routing logic had unreachable error checks (clap group already prevents conflicts)
- patch_settings_shared() purpose not explicitly documented
- Platform flags were --skip-claude/--skip-gemini (confusing double-negative)

What changed:
- init.rs: Added 42-line architecture documentation block (lines 453-492) explaining:
  - 6 shared infrastructure functions (patch_settings_shared, show_agent_hook_status, etc.)
  - Platform-specific differences (PreToolUse/Bash vs BeforeTool/run_shell_command)
  - Why differences cannot be unified (protocol variations)
  - Default behavior (both platforms)
  - Usage examples (lines 486-492)
- init.rs: Enhanced patch_settings_shared() doc comment (line 498): "Used by both Claude Code and Gemini CLI"
- main.rs: Simplified to --claude (Claude only) and --gemini (Gemini only) flags
- main.rs: Platform selection logic (lines 1089-1119): no flags = both, flag = that one only
- main.rs: Removed unreachable error checks (clap group prevents flag conflicts)

Why:
- DX: Simpler mental model (--claude vs --skip-claude double-negative)
- Maintainability: Developers can see which functions are shared (DRY) vs platform-specific
- Clarity: Documents why platform-specific functions exist (protocol differences, not duplication)
- Code size: Removes unreachable error handling (clap handles it)
- Verification: Explicit line number references enable quick navigation to shared infrastructure

DRY summary:
- Shared: 6 core functions used by both platforms
- Platform-specific: JSON structure differences (PreToolUse vs BeforeTool), artifact management (RTK.md vs none)
- Appropriate separation: Protocol-specific code isolated, common logic extracted

Usage examples:
- rtk init              → Both Claude and Gemini (default)
- rtk init --claude     → Claude only
- rtk init --gemini     → Gemini only
- rtk init --uninstall  → Remove both
- rtk init --uninstall --claude → Remove Claude only

Testable:
- cargo test — all 567 tests pass
- rtk init --claude --gemini → clap error (group conflict, at most one)
- rtk init --help — shows platform selection flags
Previous behavior:
- run_gemini() printed success message after patch_gemini_settings() already reported result
- Users saw 3 messages for Gemini: patch result + run_gemini block + multi-platform summary
- Claude only showed 1 consolidated message + summary
- Inconsistent verbosity between platforms

What changed:
- init.rs:run_gemini() (lines 1060-1064): Simplified to call patch_gemini_settings() and return
- Removed duplicate output block (18 lines deleted): "RTK Gemini CLI hook setup" header + details + restart message
- patch_settings_shared() already prints: hook status, backup info, restart instructions
- Matches Claude behavior: patch function reports, run function is silent

Why:
- Consistency: Both platforms now have 1 message each + 1 summary (when both set up)
- DRY: Dont duplicate what patch_settings_shared() already prints
- UX: Less noise, clearer output

Expected output after fix:
```
RTK hook installed (global).       # Claude
  Hook: rtk hook claude...
  settings.json: hook already present

Patch /Users/athundt/.gemini/settings.json? [y/N]  # Gemini
y
  Gemini settings.json: hook added
  Restart Gemini CLI. Test with: gemini

✓ RTK installed for both platforms  # Summary
  Restart both CLIs.
```

Testable:
- cargo test — all 567 tests pass
- rtk init -g — should show 1 Claude msg + 1 Gemini msg + 1 summary (not 3 Gemini msgs)
Previous behavior:
- Claude: Creates RTK.md + patches CLAUDE.md + patches settings.json (3 artifacts)
- Gemini: Only patches settings.json (1 artifact)
- Asymmetric workflows despite both CLIs supporting instruction files
- Code duplication: patch_claude_md and @RTK.md removal logic not shared

What changed:
- run_gemini() now mirrors run_default_mode():
  1. Creates ~/.gemini/RTK.md (10 lines, same as Claude)
  2. Patches ~/.gemini/GEMINI.md (adds @RTK.md reference)
  3. Patches ~/.gemini/settings.json (hook registration)
- uninstall_gemini() now mirrors Claude uninstall():
  - Removes RTK.md file
  - Removes @RTK.md reference from GEMINI.md
  - Removes hook from settings.json
- Extracted patch_instruction_file() (lines 833-877): Shared by patch_claude_md() and patch_gemini_md()
- Extracted remove_rtk_reference_from_file() (lines 805-830): Shared by both uninstall functions
- Updated architecture docs (lines 459-494): Symmetric workflow now documented

Why:
- Gemini CLI DOES support GEMINI.md instruction files (confirmed via https://geminicli.com/docs/cli/gemini-md/)
- Gemini CLI DOES support @ directive for file inclusion (same syntax as Claude)
- Both platforms should work identically for consistent UX
- DRY: Eliminates 38 lines of duplicated @RTK.md patching/removal logic

Research sources:
- https://geminicli.com/docs/cli/gemini-md/ — GEMINI.md documentation
- https://geminicli.com/docs/extensions/ — Extension context files
- https://claude.com/blog/using-claude-md-files — CLAUDE.md @include syntax

Testable:
- cargo test — all 567 tests pass (no regressions)
- rtk init --gemini → should create RTK.md + patch GEMINI.md + patch settings.json
- rtk init --uninstall --gemini → should remove all 3 artifacts
- ls ~/.gemini/RTK.md && cat ~/.gemini/GEMINI.md (verify files exist with @RTK.md)
Previous behavior:
- --claude and --gemini were in clap group "platform" (mutually exclusive)
- Could not use both flags together
- rtk init -g --claude --gemini → clap error
- Platform logic was verbose match expressions (not DRY)

What changed:
- main.rs: Removed group = "platform" from both flags
- main.rs: Simplified platform selection to single-line Boolean expressions:
  - setup_claude = \!gemini || claude   (true unless --gemini alone)
  - setup_gemini = \!claude || gemini   (true unless --claude alone)
- Applies to both install and uninstall paths (DRY)

Why:
- Boolean algebra: claude || (\!claude && \!gemini) reduces to \!gemini || claude
- DRY: 4 lines instead of 20+ lines of match tables
- Clarity: Logic is obvious from the expression
- Flexibility: All flag combinations now work

Flag combinations:
- rtk init                       → Both (Claude local + Gemini global)
- rtk init -g                    → Both (Claude global + Gemini global)
- rtk init --claude              → Claude only (local)
- rtk init --gemini              → Gemini only (global)
- rtk init --claude --gemini     → Both (Claude local + Gemini global)
- rtk init -g --claude --gemini  → Both (Claude global + Gemini global)

Note: -g affects Claude mode (local vs global), Gemini is always global

Testable:
- cargo test — all 567 tests pass
- rtk init --claude --gemini → works (no error)
…covery

Replace hardcoded safety rules (SafetyAction/SafetyRule/rule\!() macro) with
a unified data-driven Rule system using MD files with YAML frontmatter.

Config system:
- Split config.rs into config/{mod,discovery,rules}.rs
- Add load_merged() with precedence: CLI > env vars > project-local > global > defaults
- Add ConfigOverlay for partial config merging
- Add CRUD: get/set/unset/list subcommands for scalar config and rules
- Add --config-path, --config-add, --rules-path, --rules-add CLI flags
- Wire dead config fields: tracking.enabled, tracking.history_days, database_path

Rules system:
- 11 built-in rules as MD files compiled via include_str\!()
- Directory walk-up discovery: .claude/, .gemini/, .rtk/ from cwd to home
- Same-name override semantics (user rules override builtins)
- Predicate system with registry + bash fallback
- try_remap() for single-word alias expansion (e.g. t -> cargo test)

Safety refactor:
- Rewrite check()/check_raw() to iterate rules::load_all()
- Extract dispatch() to eliminate duplication
- Preserve SafetyResult enum (used by exec.rs)

Other:
- Export rules during rtk init for discoverability
- Fix flaky test_cd_to_existing_dir by consolidating cwd-mutating tests
- Add serde_yaml dependency for YAML frontmatter parsing
- 643 tests pass (extensive TDD coverage for precedence, robustness, CLI parsing)
Remove unused code properly with modern idiomatic Rust fixes:
- Remove BILLION constant from cc_economics.rs (unused)
- Remove is_available() function from ccusage.rs (unused)
- Remove get_all_rules_dirs() from config/mod.rs (unused)
- Remove is_safety() method from config/rules.rs (unused)
- Add #[serde(default)] to golangci_cmd fields (line, column, text)
- Add #[serde(default)] to playwright_cmd duration field
- Mark deprecated track() with #[allow(dead_code)] for backwards compatibility

All changes from this branch compile without warnings. Remaining 17 warnings
are pre-existing and unrelated to multi-platform-hooks branch.
…, add multi-pattern test

README.md:
- Add Gemini CLI alongside Claude Code in intro, name collision, installation,
  quick start, and token savings sections
- Rename "Auto-Rewrite Hook" section to "Claude Code Integration" for
  parallel structure with "Gemini CLI Integration"
- Expand safety table from 8 to 11 rows (all rule files) with per-rule
  opt-out env var column
- Add "Why these commands?" rationale, action type definitions, rule
  priority list (highest to lowest), custom rule example with chmod-777,
  rule field reference, when: condition documentation
- Add chained command before/after example with smart quoting note
- Clarify hook check output, manual install PreToolUse placement,
  Gemini matcher field, blocked commands in rewrite table
- Fix duplicate uninstall entry, default init installs both platforms
- Mention SKILL.md similarity for rule file format

src/config/rules.rs:
- Add test_matches_rule_multiple_patterns_in_one_rule test confirming
  multiple patterns in a single rule file work (chmod -R 777 + chmod 777)
… dispatcher

Merge origin/master (7401f10) into feat/multi-platform-hooks.

From master (6 commits):
- src/format_cmd.rs: add universal format command (prettier, black, ruff format)
- src/lint_cmd.rs: add Python lint dispatcher (pylint, mypy, flake8, ruff via rtk lint)
- src/git.rs: add "Not a git repository" error handling for git status
- src/ruff_cmd.rs: make filter_ruff_check_json and filter_ruff_format pub
- src/main.rs: add Format command variant and mod format_cmd
- hooks/rtk-rewrite.sh: global option stripping for git/cargo/docker/kubectl (PR rtk-ai#99)
- Cargo.toml: version bump to 0.16.0

Conflict resolution:
- hooks/rtk-rewrite.sh: kept 3-line migration shim (shell logic replaced by Rust
  handlers in this branch), integrated PR rtk-ai#99 global option stripping into Rust

Branch additions to integrate master capabilities:
- src/config/rules.rs: add strip_global_options() for git (-C, --no-pager,
  --no-optional-locks, --bare, --literal-pathspecs, --key=value), cargo (+toolchain),
  docker (-H, --context, --config), kubectl (--context, --kubeconfig, -n).
  Updated matches_rule() to normalize commands before multi-word pattern matching.
  Table-driven tests: 26 strip cases, 11 rule-matching-with-globals cases.
- src/cmd/hook.rs: add test_global_options_not_blocked (12 cases),
  test_compound_commands_rewrite (5 chain cases with operator preservation),
  test_compound_blocked_in_chain (3 cases), test_compound_quoted_operators_not_split.
- src/ccusage.rs: fix trailing whitespace (cargo fmt)

675 tests pass, 0 failures. cargo fmt clean, no new clippy warnings.
@ahundt ahundt changed the title Gemini CLI hooks, rm→trash / git→stash data safety, chained command rewriting, rtk.*.md rule engine Gemini CLI support, rm→trash / git→stash data safety, chained command rewriting, rtk.*.md rule engine Feb 15, 2026
@ahundt ahundt changed the title Gemini CLI support, rm→trash / git→stash data safety, chained command rewriting, rtk.*.md rule engine Gemini CLI support, rm→trash / git→stash data safety rtk.*.md rules, chained command rewriting, Rust-based hooks Feb 15, 2026
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.

Hook: chained commands (cd dir && cmd) are never rewritten

1 participant