mirror of
https://github.com/headroomlabs-ai/headroom.git
synced 2026-08-10 14:27:00 -04:00
fix: patch rtk hook script to use absolute, shell-quoted path after register_claude_hooks
This commit is contained in:
parent
0b4a4bd483
commit
d5987fb26b
2 changed files with 156 additions and 1 deletions
|
|
@ -18,6 +18,8 @@ import importlib.util
|
|||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import signal
|
||||
import socket
|
||||
|
|
@ -445,12 +447,64 @@ def _setup_rtk(verbose: bool = False) -> Path | None:
|
|||
if register_claude_hooks(rtk_path):
|
||||
if verbose:
|
||||
click.echo(" rtk hooks registered in Claude Code")
|
||||
try:
|
||||
patched = _patch_rtk_hook_absolute_path(rtk_path)
|
||||
if patched and verbose:
|
||||
click.echo(" rtk hook script patched to use absolute path")
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
click.echo(f" rtk hook absolute-path patch skipped: {e}")
|
||||
else:
|
||||
click.echo(" rtk hook registration failed — continuing without it")
|
||||
|
||||
return rtk_path
|
||||
|
||||
|
||||
def _patch_rtk_hook_absolute_path(rtk_path: Path, hook_script_path: Path | None = None) -> bool:
|
||||
"""Rewrite bare ``rtk`` invocations in the generated Claude hook script
|
||||
to use the absolute path to the RTK binary Headroom manages.
|
||||
|
||||
``rtk init --global --auto-patch`` writes ``~/.claude/hooks/rtk-rewrite.sh``
|
||||
with a bare ``rtk`` command that depends on PATH lookup. Since
|
||||
``~/.headroom/bin`` (where Headroom installs rtk) is not automatically
|
||||
added to PATH, that lookup fails and the hook silently does nothing.
|
||||
|
||||
This rewrites bare ``rtk`` command tokens to the absolute, shell-quoted
|
||||
path of the rtk binary so the hook works regardless of PATH.
|
||||
|
||||
Idempotent: only rewrites bare ``rtk`` tokens (not paths that already
|
||||
point elsewhere), and only writes the file back if content changed.
|
||||
|
||||
Returns True if the hook script was modified.
|
||||
"""
|
||||
if hook_script_path is None:
|
||||
hook_script_path = Path.home() / ".claude" / "hooks" / "rtk-rewrite.sh"
|
||||
|
||||
if not hook_script_path.exists():
|
||||
return False
|
||||
|
||||
original = hook_script_path.read_text()
|
||||
|
||||
# Quote the absolute path safely for POSIX shells. This matters because
|
||||
# paths containing spaces or other shell-special characters (e.g.
|
||||
# "/Users/Alice Smith/.headroom/bin/rtk") must be quoted, or the
|
||||
# generated script will break when the shell splits on whitespace.
|
||||
quoted_path = shlex.quote(str(rtk_path))
|
||||
|
||||
# Replace bare `rtk` command tokens with the quoted absolute path.
|
||||
# Matches `rtk` as a standalone word (preceded by start-of-line or
|
||||
# whitespace/operators, followed by whitespace or end-of-line), so it
|
||||
# won't touch things like "rtkfoo" or "/some/path/rtk" that are already
|
||||
# absolute.
|
||||
patched, count = re.subn(r"(?<![\w/-])rtk(?=\s|$)", quoted_path, original)
|
||||
|
||||
if count and patched != original:
|
||||
hook_script_path.write_text(patched)
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _setup_lean_ctx_agent(agent: str, verbose: bool = False) -> Path | None:
|
||||
"""Run lean-ctx agent setup for the requested coding tool."""
|
||||
|
||||
|
|
@ -4401,4 +4455,4 @@ def unwrap_codex(port: int, no_stop_proxy: bool) -> None:
|
|||
click.echo("✓ Codex is no longer routed through the Headroom proxy.")
|
||||
if not no_stop_proxy and status != "noop":
|
||||
_echo_unwrap_proxy_stop_status(_stop_local_proxy_for_unwrap(port), port)
|
||||
click.echo()
|
||||
click.echo()
|
||||
101
tests/test_cli/test_wrap_rtk_hook_patch.py
Normal file
101
tests/test_cli/test_wrap_rtk_hook_patch.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
"""Tests for ``_patch_rtk_hook_absolute_path``.
|
||||
|
||||
``rtk init --global --auto-patch`` writes ``~/.claude/hooks/rtk-rewrite.sh``
|
||||
with a bare ``rtk`` command that depends on PATH lookup. Since
|
||||
``~/.headroom/bin`` is not automatically added to PATH, that lookup fails
|
||||
silently and token compression never occurs (see issue #487).
|
||||
|
||||
``_patch_rtk_hook_absolute_path`` rewrites bare ``rtk`` tokens in the
|
||||
generated hook script to the absolute, shell-quoted path of the rtk binary
|
||||
that Headroom manages.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shlex
|
||||
from pathlib import Path
|
||||
|
||||
from headroom.cli.wrap import _patch_rtk_hook_absolute_path
|
||||
|
||||
|
||||
def test_patches_bare_rtk_to_absolute_path(tmp_path: Path) -> None:
|
||||
hook_script = tmp_path / "rtk-rewrite.sh"
|
||||
hook_script.write_text(
|
||||
"#!/bin/sh\n"
|
||||
"if command -v rtk >/dev/null 2>&1; then\n"
|
||||
" exec rtk rewrite \"$@\"\n"
|
||||
"fi\n"
|
||||
)
|
||||
|
||||
rtk_path = Path("/home/user/.headroom/bin/rtk")
|
||||
changed = _patch_rtk_hook_absolute_path(rtk_path, hook_script)
|
||||
|
||||
content = hook_script.read_text()
|
||||
assert changed is True
|
||||
assert f"exec {rtk_path} rewrite" in content
|
||||
|
||||
|
||||
def test_quotes_path_containing_spaces(tmp_path: Path) -> None:
|
||||
"""Paths with spaces (e.g. /Users/Alice Smith/...) must be shell-quoted."""
|
||||
hook_script = tmp_path / "rtk-rewrite.sh"
|
||||
hook_script.write_text(
|
||||
"#!/bin/sh\n"
|
||||
"if command -v rtk >/dev/null 2>&1; then\n"
|
||||
" exec rtk rewrite \"$@\"\n"
|
||||
"fi\n"
|
||||
)
|
||||
|
||||
rtk_path = Path("/Users/Alice Smith/.headroom/bin/rtk")
|
||||
changed = _patch_rtk_hook_absolute_path(rtk_path, hook_script)
|
||||
|
||||
content = hook_script.read_text()
|
||||
quoted = shlex.quote(str(rtk_path))
|
||||
|
||||
assert changed is True
|
||||
assert f"exec {quoted} rewrite" in content
|
||||
# The raw, unquoted path must never appear unescaped in the script.
|
||||
assert "exec /Users/Alice Smith/.headroom/bin/rtk rewrite" not in content
|
||||
|
||||
|
||||
def test_idempotent_second_run_is_noop(tmp_path: Path) -> None:
|
||||
hook_script = tmp_path / "rtk-rewrite.sh"
|
||||
hook_script.write_text("exec rtk rewrite \"$@\"\n")
|
||||
|
||||
rtk_path = Path("/home/user/.headroom/bin/rtk")
|
||||
|
||||
first = _patch_rtk_hook_absolute_path(rtk_path, hook_script)
|
||||
content_after_first = hook_script.read_text()
|
||||
|
||||
second = _patch_rtk_hook_absolute_path(rtk_path, hook_script)
|
||||
content_after_second = hook_script.read_text()
|
||||
|
||||
assert first is True
|
||||
assert second is False
|
||||
assert content_after_first == content_after_second
|
||||
|
||||
|
||||
def test_missing_hook_script_is_noop(tmp_path: Path) -> None:
|
||||
missing = tmp_path / "does-not-exist.sh"
|
||||
rtk_path = Path("/home/user/.headroom/bin/rtk")
|
||||
|
||||
changed = _patch_rtk_hook_absolute_path(rtk_path, missing)
|
||||
|
||||
assert changed is False
|
||||
assert not missing.exists()
|
||||
|
||||
|
||||
def test_does_not_touch_words_containing_rtk(tmp_path: Path) -> None:
|
||||
"""Tokens like 'rtkfoo' or an already-absolute '/some/path/rtk' are left alone."""
|
||||
hook_script = tmp_path / "rtk-rewrite.sh"
|
||||
original = (
|
||||
"#!/bin/sh\n"
|
||||
"echo rtkfoo\n"
|
||||
"exec /already/absolute/rtk rewrite \"$@\"\n"
|
||||
)
|
||||
hook_script.write_text(original)
|
||||
|
||||
rtk_path = Path("/home/user/.headroom/bin/rtk")
|
||||
changed = _patch_rtk_hook_absolute_path(rtk_path, hook_script)
|
||||
|
||||
assert changed is False
|
||||
assert hook_script.read_text() == original
|
||||
Loading…
Add table
Add a link
Reference in a new issue