RTK Hook Increases Claude Code Costs by 18%
NOTE: files and GitHub issue generated with the help of Claude
Reproduction Package: This issue includes all files needed to reproduce the bug:
test_example.py - Minimal test demonstrating pytest bug
ci_log_synthetic.txt - Error log showing the bug
requirements.txt - Dependencies (just pytest)
rtk-rewrite.sh - The problematic hook script
toggle-rtk - Enable/disable script
Quick verification: pip install -r requirements.txt && CI=true pytest test_example.py → 3 errors
Summary
RTK's PreToolUse hook for Claude Code increases costs by 18% instead of reducing them. The hook silently rewrites commands (e.g., cat → rtk read), producing compressed output that forces Claude to generate 50% more output tokens to compensate for missing information.
Expected: Compressed input → Lower cost
Actual: Compressed input → Claude writes MORE → Higher cost
Measured Impact
Identical debugging task, RTK enabled vs disabled:
| Metric |
RTK Disabled |
RTK Enabled |
Δ |
| Output tokens |
1.6k |
2.4k |
+50% |
| Total cost |
$0.68 |
$0.80 |
+18% |
| Duration |
1m 16s |
1m 37s |
+26% |
The input token savings are dwarfed by output token increases.
Root Cause
The hook uses "permissionDecision": "allow" (line 57 in rtk-rewrite.sh), which silently rewrites commands without Claude knowing:
- Claude calls
cat file.txt
- Hook rewrites to
rtk read file.txt (compressed output)
- Claude receives unexpected compressed format
- Claude writes more tokens to explain/interpret the abbreviated data
- Claude may need additional commands to get missing information
Net result: Input savings < Output increase → Higher total cost
Reproduction (Complete Self-Contained Test)
The Bug (in test_example.py)
@pytest.mark.skipif(
os.getenv("CI"), # BUG: Returns string "true", pytest tries to eval it
reason="Skip in CI environment",
)
class TestWithBug:
def test_example_one(self) -> None:
assert 1 + 1 == 2
When CI=true is set (GitHub Actions), pytest tries to evaluate the string "true" as Python code:
NameError: name 'true' is not defined
Fix: Wrap with bool(): bool(os.getenv("CI"))
Optional: Verify Bug Locally
# Install pytest
pip install -r requirements.txt
# Run WITHOUT CI (passes)
pytest test_example.py -v
# → 6 passed
# Run WITH CI (errors)
CI=true pytest test_example.py -v
# → 3 errors: NameError: name 'true' is not defined
This confirms the bug is real before testing with Claude.
Setup RTK Hook
# Install hook scripts
mkdir -p ~/.claude/hooks ~/.claude/bin
cp rtk-rewrite.sh ~/.claude/hooks/
cp toggle-rtk ~/.claude/bin/
chmod +x ~/.claude/bin/toggle-rtk
# Enable RTK
toggle-rtk
# Verify (should show hook registered)
jq '.hooks.PreToolUse[] | select(.matcher == "Bash")' ~/.claude/settings.json
Test Procedure
Prompt for both tests:
Tests failed in CI. The error log is in @ci_log_synthetic.txt and the test file is @test_example.py. Help me debug and fix the issue.
Test 1: RTK Enabled
toggle-rtk # enable if needed
claude # start fresh session
# Paste prompt above
# After Claude completes: /cost
# Record output tokens and cost
Test 2: RTK Disabled
toggle-rtk # disable
claude # start fresh session
# Paste same prompt
# After Claude completes: /cost
# Record output tokens and cost
Expected Result: RTK enabled shows ~50% more output tokens and ~18% higher cost.
Why This Happens
Design Mismatch
| RTK Design (Human Terminal) |
Claude Code Reality |
| Compressed summaries helpful |
Needs complete data for decisions |
| Reduces reading time |
Generates tokens to interpret compressed data |
| Single-shot commands |
Multi-step workflows need context |
When RTK compresses output:
- Humans benefit: Less to read
- Claude compensates: Writes more to explain what it sees, makes additional tool calls to fill gaps
Example
Without RTK (efficient):
# Claude sees full grep output with context
"ERROR at line 10: NameError: name 'true' is not defined"
# Claude responds concisely: "The bug is on line 10, wrap with bool()"
# Output: ~200 tokens
With RTK (inefficient):
# Claude sees compressed: "✓ 3 ERRORs | Pattern: 'true' undefined"
# Claude responds verbosely: "I see there are errors related to 'true' being undefined.
# Let me investigate further... This appears to be a pytest skipif condition issue...
# The pattern suggests... Let me check the test file..."
# Output: ~300 tokens (+50%)
Technical Details
Hook Location: ~/.claude/hooks/rtk-rewrite.sh
Problem Line: 57 ("permissionDecision": "allow")
RTK Version Tested: 0.29.0
Claude Model: claude-sonnet-4-5
Environment: macOS, but issue affects all platforms
Hook Mechanism:
# Hook intercepts every Bash command
CMD=$(echo "$INPUT" | jq -r '.tool_input.command')
REWRITTEN=$(rtk rewrite "$CMD") || exit 0
# If rewrite exists, auto-allow it (THIS IS THE PROBLEM)
jq -n '{
"hookSpecificOutput": {
"permissionDecision": "allow", # ← Silent substitution
"updatedInput": {command: $REWRITTEN}
}
}'
Environment
- Claude Code: Latest CLI version
- RTK: 0.29.0
- Model: claude-sonnet-4-5
- Python: 3.8+ (for test reproduction)
- pytest: 7.0+ (for test reproduction)
Conclusion
RTK's compression works as designed but causes net harm in LLM agent workflows. The 80% input compression is offset by 50% output increase, resulting in 18% higher overall costs.
The issue stems from a fundamental mismatch: RTK optimizes for human-readable summaries, but LLM agents need complete, structured data for programmatic decision-making. The silent auto-allow mechanism exacerbates the problem by preventing the LLM from adapting its behavior.
Appendix: Supporting Files
test_example.py
"""Minimal test file demonstrating the pytest skipif bug.
This file can be run standalone with: pytest test_example.py
When CI environment variable is set, the test should skip but instead fails with:
NameError: name 'true' is not defined
This is because GitHub Actions sets CI=true (string), and pytest tries to
evaluate the string "true" as Python code.
"""
import os
import pytest
# This class has the BUG - missing bool() wrapper
@pytest.mark.skipif(
os.getenv("CI"),
reason="Skip in CI environment",
)
class TestWithBug:
"""Test class with buggy skipif condition."""
def test_example_one(self) -> None:
"""Example test that should skip in CI."""
assert 1 + 1 == 2
def test_example_two(self) -> None:
"""Another test that should skip in CI."""
assert "hello" == "hello"
def test_example_three(self) -> None:
"""Third test that should skip in CI."""
assert len([1, 2, 3]) == 3
# This class has the FIX - correct bool() wrapper
@pytest.mark.skipif(
bool(os.getenv("CI")),
reason="Skip in CI environment",
)
class TestWithFix:
"""Test class with correct skipif condition."""
def test_example_one(self) -> None:
"""Example test that correctly skips in CI."""
assert 1 + 1 == 2
def test_example_two(self) -> None:
"""Another test that correctly skips in CI."""
assert "hello" == "hello"
def test_example_three(self) -> None:
"""Third test that correctly skips in CI."""
assert len([1, 2, 3]) == 3
ci_log_synthetic.txt
============================= test session starts ==============================
platform darwin -- Python 3.11.8, pytest-8.0.0, pluggy-1.4.0
cachedir: .pytest_cache
rootdir: /home/runner/work/example-project
configfile: pyproject.toml
plugins: cov-4.1.0, nbmake-1.5.0
collecting ... collected 6 items / 3 errors
==================================== ERRORS ====================================
______________ ERROR at setup of TestWithBug.test_example_one __________________
name 'true' is not defined
The above exception was the direct cause of the following exception:
def test_example_one(self) -> None:
> """Example test that should skip in CI."""
E Failed: Error evaluating 'skipif' condition
E os.getenv("CI")
E as a Python expression:
E true
E NameError: name 'true' is not defined
test_example.py:14:
______________ ERROR at setup of TestWithBug.test_example_two __________________
name 'true' is not defined
The above exception was the direct cause of the following exception:
def test_example_two(self) -> None:
> """Another test that should skip in CI."""
E Failed: Error evaluating 'skipif' condition
E os.getenv("CI")
E as a Python expression:
E true
E NameError: name 'true' is not defined
test_example.py:18:
____________ ERROR at setup of TestWithBug.test_example_three __________________
name 'true' is not defined
The above exception was the direct cause of the following exception:
def test_example_three(self) -> None:
> """Third test that should skip in CI."""
E Failed: Error evaluating 'skipif' condition
E os.getenv("CI")
E as a Python expression:
E true
E NameError: name 'true' is not defined
test_example.py:22:
test_example.py::TestWithFix::test_example_one SKIPPED (Skip in CI) [ 50%]
test_example.py::TestWithFix::test_example_two SKIPPED (Skip in CI) [ 66%]
test_example.py::TestWithFix::test_example_three SKIPPED (Skip in CI) [100%]
=========================== short test summary info ============================
ERROR test_example.py::TestWithBug::test_example_one - Failed: Error evaluating 'skipif' condition
os.getenv("CI")
as a Python expression:
true
NameError: name 'true' is not defined
ERROR test_example.py::TestWithBug::test_example_two - Failed: Error evaluating 'skipif' condition
os.getenv("CI")
as a Python expression:
true
NameError: name 'true' is not defined
ERROR test_example.py::TestWithBug::test_example_three - Failed: Error evaluating 'skipif' condition
os.getenv("CI")
as a Python expression:
true
NameError: name 'true' is not defined
==================== 3 passed, 3 skipped, 3 errors in 0.12s ====================
requirements.txt
# Minimal requirements for running the test example
pytest>=7.0.0
rtk-rewrite.sh
#!/usr/bin/env bash
# rtk-hook-version: 2
# RTK Claude Code hook — rewrites commands to use rtk for token savings.
# Requires: rtk >= 0.23.0, jq
#
# This is a thin delegating hook: all rewrite logic lives in `rtk rewrite`,
# which is the single source of truth (src/discover/[registry.rs](http://registry.rs/)).
# To add or change rewrite rules, edit the Rust registry — not this file.
if ! command -v jq &>/dev/null; then
echo "[rtk] WARNING: jq is not installed. Hook cannot rewrite commands. Install jq: https://jqlang.github.io/jq/download/" >&2
exit 0
fi
if ! command -v rtk &>/dev/null; then
echo "[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation" >&2
exit 0
fi
# Version guard: rtk rewrite was added in 0.23.0.
# Older binaries: warn once and exit cleanly (no silent failure).
RTK_VERSION=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -n "$RTK_VERSION" ]; then
MAJOR=$(echo "$RTK_VERSION" | cut -d. -f1)
MINOR=$(echo "$RTK_VERSION" | cut -d. -f2)
# Require >= 0.23.0
if [ "$MAJOR" -eq 0 ] && [ "$MINOR" -lt 23 ]; then
echo "[rtk] WARNING: rtk $RTK_VERSION is too old (need >= 0.23.0). Upgrade: cargo install rtk" >&2
exit 0
fi
fi
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
if [ -z "$CMD" ]; then
exit 0
fi
# Delegate all rewrite logic to the Rust binary.
# rtk rewrite exits 1 when there's no rewrite — hook passes through silently.
REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || exit 0
# No change — nothing to do.
if [ "$CMD" = "$REWRITTEN" ]; then
exit 0
fi
ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')
jq -n \
--argjson updated "$UPDATED_INPUT" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": $updated
}
}'
toggle-rtk
#!/usr/bin/env bash
# Toggle RTK hook in Claude Code settings
set -euo pipefail
SETTINGS_FILE="$HOME/.claude/settings.json"
BACKUP_FILE="$HOME/.claude/settings.json.bak"
RTK_HOOK_PATH="$HOME/.claude/hooks/rtk-rewrite.sh"
# Colors for output
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m' # No Color
if [[ ! -f "$SETTINGS_FILE" ]]; then
echo -e "${RED}Error: Settings file not found at $SETTINGS_FILE${NC}"
exit 1
fi
# Create backup
cp "$SETTINGS_FILE" "$BACKUP_FILE"
# Check current state
if jq -e '.hooks.PreToolUse[] | select(.matcher == "Bash") | .hooks[] | select(.command | contains("rtk-rewrite.sh"))' "$SETTINGS_FILE" > /dev/null 2>&1; then
CURRENT_STATE="enabled"
else
CURRENT_STATE="disabled"
fi
if [[ "$CURRENT_STATE" == "enabled" ]]; then
# Disable RTK: Remove the rtk hook from PreToolUse
echo -e "${YELLOW}Disabling RTK...${NC}"
jq '(.hooks.PreToolUse[] | select(.matcher == "Bash") | .hooks) |= map(select(.command | contains("rtk-rewrite.sh") | not))' \
"$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
echo -e "${GREEN}✓ RTK disabled${NC}"
echo "Restart Claude Code for changes to take effect."
echo "Run 'toggle-rtk' again to re-enable."
else
# Enable RTK: Add the rtk hook to PreToolUse
echo -e "${YELLOW}Enabling RTK...${NC}"
# Check if PreToolUse exists for Bash matcher
if jq -e '.hooks.PreToolUse[] | select(.matcher == "Bash")' "$SETTINGS_FILE" > /dev/null 2>&1; then
# Bash matcher exists, add hook to it
jq --arg cmd "$RTK_HOOK_PATH" \
'(.hooks.PreToolUse[] | select(.matcher == "Bash") | .hooks) += [{"type": "command", "command": $cmd}]' \
"$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
else
# No Bash matcher, create it with the hook
jq --arg cmd "$RTK_HOOK_PATH" \
'.hooks.PreToolUse += [{"matcher": "Bash", "hooks": [{"type": "command", "command": $cmd}]}]' \
"$SETTINGS_FILE" > "$SETTINGS_FILE.tmp" && mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
fi
echo -e "${GREEN}✓ RTK enabled${NC}"
echo "Restart Claude Code for changes to take effect."
echo "Run 'toggle-rtk' again to disable."
fi
echo ""
echo "Backup saved to: $BACKUP_FILE"
exit 0
RTK Hook Increases Claude Code Costs by 18%
Summary
RTK's PreToolUse hook for Claude Code increases costs by 18% instead of reducing them. The hook silently rewrites commands (e.g.,
cat→rtk read), producing compressed output that forces Claude to generate 50% more output tokens to compensate for missing information.Expected: Compressed input → Lower cost
Actual: Compressed input → Claude writes MORE → Higher cost
Measured Impact
Identical debugging task, RTK enabled vs disabled:
The input token savings are dwarfed by output token increases.
Root Cause
The hook uses
"permissionDecision": "allow"(line 57 inrtk-rewrite.sh), which silently rewrites commands without Claude knowing:cat file.txtrtk read file.txt(compressed output)Net result: Input savings < Output increase → Higher total cost
Reproduction (Complete Self-Contained Test)
The Bug (in test_example.py)
When
CI=trueis set (GitHub Actions), pytest tries to evaluate the string"true"as Python code:Fix: Wrap with
bool():bool(os.getenv("CI"))Optional: Verify Bug Locally
This confirms the bug is real before testing with Claude.
Setup RTK Hook
Test Procedure
Prompt for both tests:
Test 1: RTK Enabled
Test 2: RTK Disabled
Expected Result: RTK enabled shows ~50% more output tokens and ~18% higher cost.
Why This Happens
Design Mismatch
When RTK compresses output:
Example
Without RTK (efficient):
With RTK (inefficient):
Technical Details
Hook Location:
~/.claude/hooks/rtk-rewrite.shProblem Line: 57 (
"permissionDecision": "allow")RTK Version Tested: 0.29.0
Claude Model: claude-sonnet-4-5
Environment: macOS, but issue affects all platforms
Hook Mechanism:
Environment
Conclusion
RTK's compression works as designed but causes net harm in LLM agent workflows. The 80% input compression is offset by 50% output increase, resulting in 18% higher overall costs.
The issue stems from a fundamental mismatch: RTK optimizes for human-readable summaries, but LLM agents need complete, structured data for programmatic decision-making. The silent auto-allow mechanism exacerbates the problem by preventing the LLM from adapting its behavior.
Appendix: Supporting Files
test_example.py
ci_log_synthetic.txt
requirements.txt
rtk-rewrite.sh
toggle-rtk