Skip to content

fix(cli): use sys.executable for hook commands instead of hardcoded /usr/bin/python3#37

Closed
FunLay123 wants to merge 1 commit into
Mibayy:mainfrom
FunLay123:fix/ts-init-python-executable
Closed

fix(cli): use sys.executable for hook commands instead of hardcoded /usr/bin/python3#37
FunLay123 wants to merge 1 commit into
Mibayy:mainfrom
FunLay123:fix/ts-init-python-executable

Conversation

@FunLay123

Copy link
Copy Markdown

Summary

Cross-platform fix for ts init. Currently every shipped hook config hardcodes /usr/bin/python3 as the interpreter, which breaks on Windows entirely and on any non-default Python install on macOS / Linux. This PR substitutes the interpreter at install time the same way the install path is already substituted.

Problem

Every shipped hook config (hooks/tool-capture-hooks-config.json, hooks/bash-rewriter-config.json, hooks/tool-capture-codex.json, hooks/tool-capture-cursor.json, hooks/tool-capture-gemini.json) hardcodes:

"command": "/usr/bin/python3 {{TS_HOOKS_DIR}}/<hook>.py"

{{TS_HOOKS_DIR}} is substituted at install time. /usr/bin/python3 is not — it lands verbatim in the user's ~/.claude/settings.json (and equivalents). This breaks every platform where the system Python lives elsewhere:

  • Windows: /usr/bin/python3 does not exist. Every Bash tool invocation fires PreToolUse + PostToolUse hooks, both fail with /usr/bin/python3: No such file or directory. tool_capture and bash_rewriter silently no-op. Observed in the wild — see hook-error spam below.
  • macOS Homebrew / pyenv / conda users: /usr/bin/python3 is the legacy Apple Python, not the interpreter that ran ts init. Best case the hooks run on the wrong interpreter and can't import token_savior; worst case the path doesn't exist on newer macOS.
  • Linux distros where python3 lives at /usr/local/bin/python3 or inside a venv: same failure mode.

Observed error on Windows (Claude Code):

PreToolUse:Bash hook error
Failed with non-blocking status code: /usr/bin/bash: line 1: /usr/bin/python3: No such file or directory
PostToolUse:Bash hook error
Failed with non-blocking status code: /usr/bin/bash: line 1: /usr/bin/python3: No such file or directory

Fix

Introduce a {{TS_PYTHON}} placeholder alongside {{TS_HOOKS_DIR}} in all 5 shipped hook configs. Substitute it at install time with sys.executable — the exact interpreter that ran ts init. That interpreter:

  • is guaranteed to exist (it's literally running the installer),
  • is the one that has token_savior (and the hook scripts' dependencies) installed,
  • requires no platform-specific detection logic.

Both placeholders go through a small _as_json_string helper (json.dumps(value)[1:-1]) so values with backslashes or quotes remain valid JSON-string content after the textual .replace. Required on Windows where sys.executable and the hooks dir both contain \. POSIX behavior is unchanged — the helper is a no-op for strings that don't need escaping.

Files changed

  • hooks/tool-capture-hooks-config.json (claude)
  • hooks/bash-rewriter-config.json (claude)
  • hooks/tool-capture-codex.json
  • hooks/tool-capture-cursor.json
  • hooks/tool-capture-gemini.json
  • src/token_savior/cli_init/__init__.py

Migration note

Users who already ran ts init on a system where /usr/bin/python3 happened to be a valid path will, on next ts init, see the command value change to the absolute sys.executable path. Because the merger dedup keys include the full command string, the old /usr/bin/python3 ... entry won't be deduplicated against the new one — a second entry is added. Users on that path should remove the stale /usr/bin/python3 entry manually. (Cleaner migration could be a follow-up.)

Test plan

  • Manual smoke test on Windows for all 4 supported agents (claude / cursor / gemini / codex): _load_hook_bundles substitutes sys.executable correctly, the resulting command's python binary actually exists on disk.
  • pytest tests/test_cli_init.py — existing tests (test_load_hook_bundles_claude_real_repo, test_apply_bundles_combines_post_and_pre, e2e test_run_*) exercise this code path and should continue to pass; on POSIX with /usr/bin/python3 as sys.executable the merged settings change only in the command's python prefix value, not in structure.
  • Reinstall + verify hooks actually fire on Windows (Bash tool → no more python3: No such file or directory).

Relation to #36

Independent of #36 but solves a related layer of the same Windows-incompatibility story:

Both PRs reuse the same json.dumps(value)[1:-1] escape technique; this PR factors it out into _as_json_string for clarity. Either PR can merge first; they don't conflict.

…usr/bin/python3

Problem
-------
Every shipped hook config (claude, codex, cursor, gemini) hardcodes the
hook command as

    "command": "/usr/bin/python3 {{TS_HOOKS_DIR}}/<hook>.py"

The {{TS_HOOKS_DIR}} placeholder is substituted at install time, but the
interpreter path is not -- it lands verbatim in the user's settings file.
The /usr/bin/python3 assumption breaks on every platform where the system
Python is not at that exact path:

  - Windows: no /usr/bin/python3 exists at all. Every Bash tool invocation
    fires PreToolUse + PostToolUse hooks that both fail with
    `/usr/bin/python3: No such file or directory`. tool_capture and the
    bash rewriter silently no-op.
  - macOS Homebrew / pyenv / conda users: /usr/bin/python3 is the legacy
    Apple-shipped Python, not the interpreter that installed token-savior.
    Best case the hooks run on the wrong interpreter and cannot import
    `token_savior`; worst case it does not exist on newer macOS.
  - Linux distros where python3 lives at /usr/local/bin/python3 or inside
    a venv: same failure mode.

Fix
---
Introduce a {{TS_PYTHON}} placeholder alongside {{TS_HOOKS_DIR}} and
substitute it at install time with `sys.executable` -- i.e. the exact
interpreter that ran `ts init`. That interpreter is guaranteed to exist
and is the one that has `token_savior` (and therefore the hook scripts'
dependencies) installed.

Both placeholders are now passed through a small `_as_json_string` helper
(`json.dumps(value)[1:-1]`) so values containing backslashes or quotes
remain valid JSON-string content after substitution. This is required on
Windows, where `sys.executable` and the hooks dir both contain `\`.

POSIX behavior is unchanged for paths without characters that need
escaping (the helper is a no-op). On platforms where the prior hardcoded
path happened to be correct (Linux systems with /usr/bin/python3), users
running `ts init` again after this change will see the command value
update to the absolute interpreter path -- existing entries that still
reference /usr/bin/python3 may need to be removed manually since the
merger dedup keys include the full command string.
@FunLay123 FunLay123 closed this May 22, 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.

1 participant