claude-code-cache-fix 3.5.0 → 3.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-cache-fix",
3
- "version": "3.5.0",
3
+ "version": "3.5.2",
4
4
  "description": "Cache optimization proxy and interceptor for Claude Code. Fixes prompt cache bugs, stabilizes prefix, reduces quota burn.",
5
5
  "type": "module",
6
6
  "exports": "./preload.mjs",
@@ -151,6 +151,17 @@ export default {
151
151
  description: "Extract cache stats from response stream, persist quota state to ~/.claude/quota-status/{account.json,sessions/<filename>.json}",
152
152
  order: 600,
153
153
 
154
+ async onRequest(ctx) {
155
+ // Session-id headers (x-claude-code-session-id, etc.) live on the
156
+ // REQUEST, not the response — Anthropic doesn't echo them back. So we
157
+ // capture them here, in the request-side hook, and stash on ctx.meta
158
+ // for onStreamEvent to use when it writes the per-session file. The
159
+ // proxy server passes the same `meta` object through onRequest →
160
+ // onResponseStart → onStreamEvent, so this works end-to-end.
161
+ if (!ctx.headers) return;
162
+ ctx.meta._sessionId = resolveSessionId(ctx.headers);
163
+ },
164
+
154
165
  async onResponseStart(ctx) {
155
166
  if (!ctx.headers) return;
156
167
 
@@ -158,7 +169,6 @@ export default {
158
169
  if (!quota) return;
159
170
 
160
171
  ctx.meta._quotaData = quota;
161
- ctx.meta._sessionId = resolveSessionId(ctx.headers);
162
172
  },
163
173
 
164
174
  async onStreamEvent(ctx) {
@@ -9,11 +9,24 @@
9
9
  # CC pipes hook input as JSON on stdin including `session_id`, which we map to
10
10
  # the per-session filename via the canonical rule (matches the writer in
11
11
  # proxy/extensions/cache-telemetry.mjs:sessionFilename).
12
-
13
- input=$(cat)
12
+ #
13
+ # Security (v3.5.2, #108): the previous version interpolated stdin into a
14
+ # Python triple-quoted literal via "''$input''", which lets a `'''` byte
15
+ # sequence in the payload close the literal early and execute arbitrary
16
+ # Python. CC's hook payload reflects user-controlled paths (cwd,
17
+ # workspace.current_dir, transcript_path), and apostrophes are legal in
18
+ # filenames, so a hostile directory name on disk could trigger code
19
+ # execution on every CC statusline redraw. We capture stdin in bash, export
20
+ # it to the env, and pass the python source through a single-quoted heredoc
21
+ # (`<<'PYEOF'`) which disables ALL bash interpolation in the body. Python
22
+ # reads the JSON via os.environ.get('CC_INPUT'), where the bytes are inert.
23
+
24
+ # Capture stdin in bash before the python heredoc consumes the stdin slot,
25
+ # then export so the python child sees it.
26
+ CC_INPUT=$(cat)
27
+ export CC_INPUT
14
28
 
15
29
  ACCOUNT="$HOME/.claude/quota-status/account.json"
16
- SESSIONS_DIR="$HOME/.claude/quota-status/sessions"
17
30
 
18
31
  # Show quota even if no per-session file exists yet (fresh session, first
19
32
  # request hasn't fired). Per-session block just gets blank.
@@ -21,7 +34,12 @@ if [ ! -f "$ACCOUNT" ]; then
21
34
  exit 0
22
35
  fi
23
36
 
24
- result=$(python3 -c "
37
+ # IMPORTANT: the heredoc tag is single-quoted (`<<'PYEOF'`). This disables
38
+ # all bash interpolation inside the heredoc body. Do NOT change to `<<PYEOF`
39
+ # without a matching audit — that would re-introduce the injection vector
40
+ # the v3.5.2 hotfix closed. The python source must reference CC_INPUT only
41
+ # through os.environ, never via a shell-substituted string.
42
+ result=$(python3 <<'PYEOF' 2>/dev/null
25
43
  import sys, json, os, re, hashlib
26
44
  from datetime import datetime, timezone, timedelta
27
45
 
@@ -34,14 +52,14 @@ sessions_dir = os.path.join(home, '.claude', 'quota-status', 'sessions')
34
52
  # canonical rule decides — the writer maps all those to 'unknown',
35
53
  # the reader must do the same to keep the contract identical.
36
54
  try:
37
- stdin_data = json.loads('''$input''') if '''$input''' else {}
55
+ stdin_data = json.loads(os.environ.get('CC_INPUT') or '{}')
38
56
  except Exception:
39
57
  stdin_data = {}
40
58
  sess_id_raw = stdin_data.get('session_id')
41
59
 
42
60
  # Canonical filename derivation — must match cache-telemetry.mjs:sessionFilename.
43
61
  # Allowlist: [A-Za-z0-9_-]{1,128}; else inv-<sha256(s)[:16]>; null/empty/whitespace -> 'unknown'.
44
- SAFE = re.compile(r'^[A-Za-z0-9_-]{1,128}\$')
62
+ SAFE = re.compile(r'^[A-Za-z0-9_-]{1,128}$')
45
63
  def session_filename(raw):
46
64
  if raw is None:
47
65
  return 'unknown'
@@ -121,6 +139,7 @@ if peak:
121
139
  label += ' | \033[33mPEAK\033[0m'
122
140
 
123
141
  print(label)
124
- " 2>/dev/null)
142
+ PYEOF
143
+ )
125
144
 
126
145
  [ -n "$result" ] && echo "$result"