Skip to content

fix(cli): JSON-escape TS_HOOKS_DIR substitution so ts init works on Windows#36

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

fix(cli): JSON-escape TS_HOOKS_DIR substitution so ts init works on Windows#36
FunLay123 wants to merge 1 commit into
Mibayy:mainfrom
FunLay123:fix/ts-init-windows-paths

Conversation

@FunLay123

Copy link
Copy Markdown

Summary

ts init is currently unusable on Windows for every supported agent (claude, cursor, gemini, codex). Root cause is in _load_hook_bundles (src/token_savior/cli_init/__init__.py).

Problem

The function substitutes the {{TS_HOOKS_DIR}} placeholder into shipped hook configs by a plain string replace, then runs json.loads on the result. The placeholder sits inside a JSON string literal:

"command": "/usr/bin/python3 {{TS_HOOKS_DIR}}/tool_capture_hook.py"

On POSIX the resolved path has no characters that need JSON-escaping, so this works. On Windows the resolved path looks like

E:\Misc\repo\token-savior\hooks

and the unescaped backslashes produce invalid \escape sequences inside the JSON string, making json.loads raise:

JSONDecodeError: Invalid \escape: ...

Reproduced locally — without the fix, _load_hook_bundles raises for all 4 supported agents.

Fix

Pass the resolved path through json.dumps and strip the surrounding quotes before substituting. The result is valid JSON-string content (backslashes doubled, quotes escaped), so the substituted text remains parseable JSON, and json.loads decodes the doubled backslashes back into a single \ in the final dict.

POSIX behavior is unchanged — for paths without characters that need escaping, json.dumps(s)[1:-1] == s.

Test plan

  • Manual reproduction: confirmed pre-fix raises JSONDecodeError on Windows for claude / cursor / gemini / codex hook bundles
  • Manual verification: post-fix _load_hook_bundles returns valid dicts for all 4 agents on Windows; substituted command strings contain correct paths
  • pytest tests/test_cli_init.py (existing test_load_hook_bundles_claude_real_repo and test_apply_bundles_combines_post_and_pre exercise this code path — they previously could only have failed on a Windows CI runner)
  • Smoke test on POSIX to confirm no regression (expected: identical behavior, since json.dumps(s)[1:-1] == s for backslash-free strings)

@FunLay123 FunLay123 force-pushed the fix/ts-init-windows-paths branch from dad2627 to 9f4092c Compare May 21, 2026 09:41
… Windows

Problem
-------
`_load_hook_bundles` substitutes the {{TS_HOOKS_DIR}} placeholder into
shipped hook configs by a plain string replace, then runs json.loads
on the result. The placeholder sits inside a JSON string literal:

    "command": "/usr/bin/python3 {{TS_HOOKS_DIR}}/tool_capture_hook.py"

On POSIX the resolved path has no characters that need JSON-escaping,
so this works. On Windows the resolved path looks like

    E:\Misc\repo\token-savior\hooks

and the unescaped backslashes produce invalid \escape sequences inside
the JSON string, making json.loads raise

    JSONDecodeError: Invalid \escape: ...

The net effect: `ts init` was unusable on Windows for every supported
agent (claude, cursor, gemini, codex), since every shipped hook config
references {{TS_HOOKS_DIR}}.

Fix
---
Pass the resolved path through json.dumps and strip the surrounding
quotes before substituting. That yields a value whose contents are
already valid JSON-string content (backslashes doubled, quotes
escaped), so the substituted text remains parseable JSON, and
json.loads decodes the doubled backslashes back into a single \
in the final dict.

POSIX behavior is unchanged -- for paths without characters that need
escaping, json.dumps(s)[1:-1] == s.
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