Skip to content

RTK Hook Increases Claude Code Costs by 18% #582

Description

@shb84

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., catrtk 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:

  1. Claude calls cat file.txt
  2. Hook rewrites to rtk read file.txt (compressed output)
  3. Claude receives unexpected compressed format
  4. Claude writes more tokens to explain/interpret the abbreviated data
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions