Skip to content

Resolve UUID connectors from only the newest session file#201

Merged
zeus-12 merged 1 commit into
mainfrom
vv/hook-latest-session-file
Jul 3, 2026
Merged

Resolve UUID connectors from only the newest session file#201
zeus-12 merged 1 commit into
mainfrom
vv/hook-latest-session-file

Conversation

@zeus-12

@zeus-12 zeus-12 commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What

Fixes a performance regression in the Claude-desktop connector resolver (_resolve_claude_code_session_connector) that could silently drop UUID→name resolution on device.

Problem

Since we added local-agent-mode-sessions (CoWork) to the resolver, it globbed **/local_*.json across both folders, stat()-ed every match to sort, and read all of them — on every mcp__ tool call. CoWork grows a deep outputs/ subtree per session (~1,200 fs entries locally for 30 session files), so the recursive walk + stat-all + read-all adds latency that scales with usage and can breach the hook timeout → the call proceeds with the raw UUID (no resolution, no policy). This is the likely reason UUID entries kept appearing after the two-folder change.

Fix

remoteMcpServersConfig is the desktop-wide connector registry, snapshotted into every session file, so the single newest local_*.json already holds the current list. So:

  • Glob only the known depth <sessions-dir>/*/*/local_*.json — never descends into outputs/.
  • Track the newest file and read only that one.

Verified on-device: bounded glob finds the exact same files as recursive (2 and 30), resolves all connectors correctly (Google Calendar / Linear / Notion), ~1.9ms vs ~23ms+.

Note

If a connector is invoked before it lands in the newest snapshot, resolve returns None (that call stays a UUID) — acceptable since the newest file reflects the current registry. py_compile clean.


Note

Low Risk
Localized performance fix in hook MCP metadata resolution; behavior change is limited to using the newest session snapshot only, with a brief window where resolution may miss brand-new connectors.

Overview
Speeds up UUID→desktop-connector resolution on every mcp__ PreToolUse path by changing how _resolve_claude_code_session_connector finds Claude session snapshots.

Instead of recursively globbing **/local_*.json (which walked CoWork outputs/ trees), stat-ing every match, sorting, and JSON-parsing each file until a UUID hit, the resolver now globs only */*/local_*.json under each session root, picks the single newest file by creation/mtime, and reads only that file’s remoteMcpServersConfig. That matches the assumption that the desktop connector registry is duplicated in every session file, so the latest snapshot is enough.

Trade-off: if a connector isn’t in the newest snapshot yet, resolution returns None for that call (raw UUID, no policy metadata) until the registry appears there.

Reviewed by Cursor Bugbot for commit 76188fe. Bugbot is set up for automated code reviews on this repo. Configure here.

Greptile Summary

Optimises _resolve_claude_code_session_connector by switching from a recursive **/local_*.json glob that stat-called every file in both session directories to a fixed-depth */*/local_*.json glob that reads only the single newest file, bringing per-call latency from ~23 ms to ~1.9 ms and preventing hook timeouts that caused UUID pass-through.

  • Glob depth change: **/local_*.json*/*/local_*.json bounds the walk to exactly 2 levels under each session directory, avoiding the deep outputs/ subtree added by local-agent-mode-sessions.
  • Single-file read: The sort-and-iterate loop over all files is replaced with a single-pass max-timestamp scan; only the winning file is read, relying on the fact that remoteMcpServersConfig is a desktop-wide snapshot present in every session file.
  • Behavioral trade-off: If the newest session file is corrupted or does not yet contain a freshly-added connector, resolution returns None with no fallback; two new early-return paths are currently silent (no log on parse failure, no log when no files are found).

Confidence Score: 4/5

Safe to merge; the performance fix is correct and well-reasoned, with the only concerns being two silent return None paths that reduce debuggability.

The core logic change is sound and the on-device verification is convincing. The two unlogged early exits — a failed JSON parse on the sole read target and the case where the bounded glob finds nothing — make it harder to distinguish a corrupt session file from a legitimately absent connector in production logs.

claude-code/hooks/unbound.py — specifically the two new return None paths (parse failure and no-files-found) that are currently silent.

Important Files Changed

Filename Overview
claude-code/hooks/unbound.py Replaces recursive **/local_.json glob + read-all with a bounded-depth //local_.json glob that reads only the single newest file. Logic is correct and the performance gain is well-motivated; two new return None paths (parse failure, no files found) are silent with no log, reducing debuggability on the hot path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A["_resolve_claude_code_session_connector(server_uuid)"] --> B{Is server_uuid a UUID?}
    B -- No --> Z1["return None"]
    B -- Yes --> C["Iterate _claude_session_dirs()\n(claude-code-sessions, local-agent-mode-sessions)"]
    C --> D["base.glob('*/*/local_*.json')\n(fixed depth — skips outputs/ subtree)"]
    D --> E["For each candidate:\n_session_file_created_at(f)"]
    E --> F{ts > latest_ts?}
    F -- Yes --> G["Update latest, latest_ts"]
    F -- No --> E
    G --> E
    E --> H{latest is None?}
    H -- Yes --> Z2["return None\n⚠ silent — no log"]
    H -- No --> I["latest.read_text() → json.loads()"]
    I -- parse error --> Z3["return None\n⚠ silent — no log"]
    I -- success --> J["Iterate remoteMcpServersConfig entries"]
    J --> K{entry.uuid matches?}
    K -- No --> J
    K -- Yes --> L["Build cfg dict\n(name, url, type, scope)"]
    L --> M["return (name, cfg)"]
    J -- exhausted --> Z4["return None"]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A["_resolve_claude_code_session_connector(server_uuid)"] --> B{Is server_uuid a UUID?}
    B -- No --> Z1["return None"]
    B -- Yes --> C["Iterate _claude_session_dirs()\n(claude-code-sessions, local-agent-mode-sessions)"]
    C --> D["base.glob('*/*/local_*.json')\n(fixed depth — skips outputs/ subtree)"]
    D --> E["For each candidate:\n_session_file_created_at(f)"]
    E --> F{ts > latest_ts?}
    F -- Yes --> G["Update latest, latest_ts"]
    F -- No --> E
    G --> E
    E --> H{latest is None?}
    H -- Yes --> Z2["return None\n⚠ silent — no log"]
    H -- No --> I["latest.read_text() → json.loads()"]
    I -- parse error --> Z3["return None\n⚠ silent — no log"]
    I -- success --> J["Iterate remoteMcpServersConfig entries"]
    J --> K{entry.uuid matches?}
    K -- No --> J
    K -- Yes --> L["Build cfg dict\n(name, url, type, scope)"]
    L --> M["return (name, cfg)"]
    J -- exhausted --> Z4["return None"]
Loading

Reviews (1): Last reviewed commit: "Resolve connectors from only the newest ..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

The connector resolver globbed **/local_*.json across both session folders,
sorted (stat-ing) every match, and read all of them. CoWork's
local-agent-mode-sessions grows a deep outputs/ subtree per session (~1200
entries locally), so on every mcp__ call the recursive walk + stat-all + read-all
added latency that scales with usage and can breach the hook timeout — silently
skipping UUID resolution (and policy) for that call.

remoteMcpServersConfig is the desktop-wide connector registry snapshotted into
every session file, so the single newest local_*.json already holds the current
list. Glob only the known depth (<dir>/*/*/local_*.json) so we never descend into
outputs/, track the newest file, and read just that one. ~1.9ms vs ~23ms+.
@zeus-12 zeus-12 requested a review from a team July 3, 2026 09:49

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 76188fe. Configure here.

try:
data = json.loads(latest.read_text(encoding='utf-8'))
except Exception:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Newest session read failure

Medium Severity

After picking the newest local_*.json, any I/O or JSON parse error on that file makes _resolve_claude_code_session_connector return None immediately. The prior implementation skipped bad files and kept searching older session snapshots, so a transient corrupt or half-written newest file can leave MCP calls on a raw UUID with no policy even when older files still resolve the connector.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 76188fe. Configure here.

@vigneshsubbiah16 vigneshsubbiah16 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛡️ Automated Security Review (consensus)

2 findings — 1 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.

🔴 Single-snapshot resolver fails open on corrupt or attacker-controlled newest file

claude-code/hooks/unbound.py:939-942

Impact: The resolver now reads only the newest local_*.json and returns None on JSON parse failure or UUID miss, so a corrupt or attacker-planted snapshot disables connector resolution for all subsequent mcp__ calls (raw UUID → no policy metadata); the sessions tree is user-writable and timestamps are trivially spoofable, and parse failures are silent.

Fix: On parse failure or miss in the newest file, fall back to the next-newest bounded-glob candidates (cap reads at 2–3 files) and log a warning when the newest snapshot is unusable.

Flagged by: Claude, Cursor (lead)

🟡 Overly permissive file permissions (pre-existing, outside diff)

claude-code/hooks/unbound.py:1761, claude-code/hooks/unbound.py:2061

Impact: 0o755 / $BITS file modes may grant broader local filesystem access than necessary for hook artifacts.

Fix: Use 0o644 (or tighter) unless the execute bit is required.

Flagged by: Semgrep

Previously acknowledged (not re-flagged)

  • Brief resolution miss for brand-new connectors not yet in the newest snapshot — Accepted in PR description: that call stays a UUID until the registry appears in the latest snapshot.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 76188fe1 · 2026-07-03T09:51Z

Comment thread claude-code/hooks/unbound.py
Comment on lines +935 to 936
if latest is None:
return None

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 No log entry when no session files are found

When latest is None (either because the session directories are empty or because no */*/local_*.json files matched), the function returns None silently. With the fixed-depth glob replacing the old **/ recursive glob, a depth mismatch — e.g., a future Claude version that nests files one level deeper — would produce exactly this silent None on every call, giving no indication in logs that the glob pattern is the problem.

@zeus-12 zeus-12 merged commit 509c69e into main Jul 3, 2026
4 checks passed
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.

3 participants