-
Notifications
You must be signed in to change notification settings - Fork 0
Land RTK across Claude, Grok, and Codex #20
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| ## RTK | ||
|
|
||
| Prefix supported shell commands with `rtk` so command output is compressed before it hits context (`rtk git status`, `rtk rg PATTERN`, `rtk test`, `rtk lint`). If RTK has no filter, it passes through unchanged. Claude Code rewrites Bash automatically; still prefix `rtk` in Codex, Grok, and T3 if a rewrite hook does not fire. | ||
|
|
||
| Meta commands (always `rtk` directly): `rtk gain`, `rtk gain --history`, `rtk proxy <cmd>`. If `rtk gain` fails, the wrong `rtk` package is on PATH. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| # RTK - Rust Token Killer | ||
|
|
||
| **Usage**: Token-optimized CLI proxy (cuts up to 90% of bash output) | ||
|
|
||
| ## Meta Commands (always use rtk directly) | ||
|
|
||
| ```bash | ||
| rtk gain # Show token savings analytics | ||
| rtk gain --history # Show command usage history with savings | ||
| rtk discover # Analyze Claude Code history for missed opportunities | ||
| rtk proxy <cmd> # Execute raw command without filtering (for debugging) | ||
| ``` | ||
|
|
||
| ## Installation Verification | ||
|
|
||
| ```bash | ||
| rtk --version # Should show: rtk X.Y.Z | ||
| rtk gain # Should work (not "command not found") | ||
| which rtk # Verify correct binary | ||
| ``` | ||
|
|
||
| ⚠️ **Name collision**: If `rtk gain` fails, you may have reachingforthejack/rtk (Rust Type Kit) installed instead. | ||
|
|
||
| ## Hook-Based Usage | ||
|
|
||
| Claude Code rewrites Bash through `rtk hook claude`. Grok/T3 and Codex use `~/.local/bin/rtk-pretooluse`, which normalizes the payload and then calls that same hook. | ||
|
|
||
| Example: `git status` → `rtk git status` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,67 @@ | ||
| #!/usr/bin/env python3 | ||
| """Merge the RTK PreToolUse adapter into ~/.claude/settings.json without taking over the file.""" | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import os | ||
| import sys | ||
|
|
||
| ADAPTER = os.path.expanduser("~/.local/bin/rtk-pretooluse") | ||
| HOOK_ENTRY = { | ||
| "matcher": "Bash", | ||
| "hooks": [{"type": "command", "command": ADAPTER}], | ||
| } | ||
|
|
||
|
|
||
| def _is_rtk_entry(entry: object) -> bool: | ||
| if not isinstance(entry, dict): | ||
| return False | ||
| for hook in entry.get("hooks") or []: | ||
| if not isinstance(hook, dict): | ||
| continue | ||
| command = hook.get("command") | ||
| if not isinstance(command, str): | ||
| continue | ||
| if "rtk hook claude" in command or command.endswith("rtk-pretooluse") or command == ADAPTER: | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| def main() -> int: | ||
| raw = sys.stdin.read() | ||
| if not raw.strip(): | ||
| data = {} | ||
| original = None | ||
| else: | ||
| original = raw | ||
| data = json.loads(raw) | ||
|
|
||
| hooks = data.setdefault("hooks", {}) | ||
| if not isinstance(hooks, dict): | ||
| hooks = {} | ||
| data["hooks"] = hooks | ||
| pre = hooks.get("PreToolUse") | ||
| if not isinstance(pre, list): | ||
| pre = [] | ||
| hooks["PreToolUse"] = pre | ||
|
|
||
| existing = [entry for entry in pre if _is_rtk_entry(entry)] | ||
| already = ( | ||
| len(existing) == 1 | ||
| and existing[0].get("matcher") == "Bash" | ||
| and (existing[0].get("hooks") or [{}])[0].get("command") == ADAPTER | ||
| and sum(1 for entry in pre if _is_rtk_entry(entry)) == 1 | ||
| ) | ||
| if already and original is not None: | ||
| sys.stdout.write(original) | ||
| return 0 | ||
|
|
||
| pre[:] = [entry for entry in pre if not _is_rtk_entry(entry)] | ||
| pre.append(HOOK_ENTRY) | ||
| json.dump(data, sys.stdout, indent=2) | ||
| sys.stdout.write("\n") | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "Bash", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "{{ .chezmoi.homeDir }}/.local/bin/rtk-pretooluse", | ||
| "statusMessage": "RTK rewrite", | ||
| "timeout": 5 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "Bash|run_terminal_command", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "{{ .chezmoi.homeDir }}/.local/bin/rtk-pretooluse", | ||
| "timeout": 5 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| #!/usr/bin/env python3 | ||
| """Normalize PreToolUse payloads and rewrite shell commands through `rtk hook claude`. | ||
|
|
||
| Claude Code sends snake_case `tool_name` / `tool_input`. Grok sends camelCase | ||
| `toolName` / `toolInput` and names the shell tool `run_terminal_command`. | ||
| `rtk hook claude` only rewrites the Claude shape, so Grok (and T3-through-Grok) | ||
| silently no-op without this adapter. | ||
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import json | ||
| import subprocess | ||
| import sys | ||
|
|
||
|
|
||
| def _command(payload: object) -> str | None: | ||
| if not isinstance(payload, dict): | ||
| return None | ||
| tool_input = payload.get("tool_input") | ||
| if not isinstance(tool_input, dict): | ||
| tool_input = payload.get("toolInput") | ||
| if not isinstance(tool_input, dict): | ||
| return None | ||
| command = tool_input.get("command") | ||
| return command if isinstance(command, str) and command.strip() else None | ||
|
|
||
|
|
||
| def main() -> int: | ||
| raw = sys.stdin.buffer.read() | ||
| if not raw.strip(): | ||
| return 0 | ||
| try: | ||
| payload = json.loads(raw) | ||
| except json.JSONDecodeError: | ||
| return 0 | ||
| command = _command(payload) | ||
| if command is None: | ||
| return 0 | ||
| claude_payload = { | ||
| "hook_event_name": "PreToolUse", | ||
| "tool_name": "Bash", | ||
| "tool_input": {"command": command}, | ||
| } | ||
|
Comment on lines
+38
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Preserve the complete Bash tool input when normalizing the payload. The registered Claude, Codex, and Grok hooks can receive Bash inputs with fields beyond 🤖 Prompt for AI Agents |
||
| try: | ||
| result = subprocess.run( | ||
| ["rtk", "hook", "claude"], | ||
| input=json.dumps(claude_payload).encode(), | ||
| capture_output=True, | ||
| check=False, | ||
| ) | ||
|
Comment on lines
+45
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Add a timeout to the Claude Code bounds Proposed fix result = subprocess.run(
["rtk", "hook", "claude"],
input=json.dumps(claude_payload).encode(),
capture_output=True,
check=False,
+ timeout=5,
)
- except OSError:
+ except (OSError, subprocess.TimeoutExpired):
return 0🤖 Prompt for AI Agents |
||
| except OSError: | ||
| return 0 | ||
| if result.returncode != 0 or not result.stdout.strip(): | ||
| return 0 | ||
| sys.stdout.buffer.write(result.stdout) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| raise SystemExit(main()) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Keep non-RTK hooks in mixed entries.
_is_rtk_entryreturns true when any hook in an entry matches RTK. Line 59 then removes the whole entry. For example, an entry containing an audit hook andrtk hook claudeloses the audit hook on the next apply. Remove only matching RTK hook objects. Keep the parent entry when it still has other hooks.🧰 Tools
🪛 Biome (2.5.8)
[error] 59-59: String values must be double quoted.
(parse)
[error] 59-59: Expected an array, an object, or a literal but instead found ':'.
(parse)
[error] 59-59: End of file expected
(parse)
[error] 59-59: unexpected character
=(parse)
[error] 59-59: String values must be double quoted.
(parse)
[error] 59-59: expected
,but instead foundfor(parse)
[error] 59-59: expected
,but instead foundentry(parse)
[error] 59-59: expected
,but instead foundin(parse)
[error] 59-59: expected
,but instead foundpre(parse)
[error] 59-59: expected
,but instead foundif(parse)
[error] 59-59: expected
,but instead foundnot(parse)
[error] 59-59: expected
,but instead found_is_rtk_entry(parse)
[error] 59-59: unexpected character
((parse)
[error] 59-59: expected
,but instead foundentry(parse)
[error] 59-59: End of file expected
(parse)
[error] 59-59: unexpected character
)(parse)
🤖 Prompt for AI Agents