Skip to content

WEB-5030: report Cowork hook sessions as unbound_app_label='cowork'#202

Merged
pugazhendhi-m merged 3 commits into
stagingfrom
WEB-5030
Jul 3, 2026
Merged

WEB-5030: report Cowork hook sessions as unbound_app_label='cowork'#202
pugazhendhi-m merged 3 commits into
stagingfrom
WEB-5030

Conversation

@pugazhendhi-m

@pugazhendhi-m pugazhendhi-m commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Linear: WEB-5030

What

One hook script (claude-code/hooks/unbound.py) serves both Claude Code and Cowork, and it hardcoded unbound_app_label='claude-code', so Cowork activity was counted as Claude Code. This makes it report Cowork under its own label.

Detection

_unbound_app_label(event):

  1. Env markers (authoritative, set by Claude Desktop): CLAUDE_CODE_IS_COWORK=1, or CLAUDE_CODE_ENTRYPOINT in local-agent | local_agent | remote_cowork.
  2. Fallback (builds predating those env vars): cwd/transcript_path under local-agent-mode-sessions.
  3. Else claude-code. Fail-open (env read wrapped; path check can't raise).

Applied on pre_tool, user_prompt, and the end-of-turn exchange (the exchange previously carried no app label).

Consolidation

Supersedes #197 (WEB-5027) — folds in its env-var detection, adds the path fallback and the missing end-of-turn label. Close #197 in favor of this.

Verification

_unbound_app_label validated against real captured events from CLI, desktop Code, and Cowork (all 3 surfaces). 7 unit tests in test_pretool_mcp.py.

Requires the gateway to accept cowork (companion ai-gateway PR) — old gateways drop unknown labels from their label-keyed maps.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RjaRWNPjy4N7KRQUykQape


Note

Low Risk
Additive telemetry labeling with fail-open fallback to claude-code; misdetection only affects analytics/policy scoping until the gateway accepts cowork.

Overview
Fixes misclassification where the shared Claude hook always sent unbound_app_label: claude-code, so Cowork activity was lumped in with Claude Code.

Adds _unbound_app_label(event) with fail-open detection: Cowork env markers (CLAUDE_CODE_IS_COWORK=1, or CLAUDE_CODE_ENTRYPOINT in local-agent / local_agent / remote_cowork), then a path fallback when cwd or transcript_path contains local-agent-mode-sessions, otherwise claude-code.

Wires that helper into pre-tool, user-prompt, and end-of-turn gateway payloads (the stop exchange previously had no app label). unbound-hook and unbound-discovery versions bump to 0.1.9; unit tests cover the detection branches.

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

Greptile Summary

This PR fixes Cowork hook sessions being incorrectly reported as claude-code by replacing the hardcoded unbound_app_label='claude-code' with a new _unbound_app_label(event) helper that uses a layered detection strategy (env vars → path fallback → default).

  • New _unbound_app_label: Checks CLAUDE_CODE_IS_COWORK=1, then CLAUDE_CODE_ENTRYPOINT membership, then a local-agent-mode-sessions substring in cwd/transcript_path, falling back to 'claude-code'. The env checks are wrapped in a fail-open try/except.
  • Wiring: The label is now applied in process_pre_tool_use, process_user_prompt_submit, and process_stop_event (the stop-event exchange was previously unlabeled entirely).
  • Tests: 8 new unit tests in TestUnboundAppLabel covering CLI, desktop, env markers, non-Cowork entrypoints, path fallback, and missing-field defaults. Version bumped to 0.1.9 in both __init__.py and the discovery entry.

Confidence Score: 5/5

Additive label-detection change with fail-open fallback; misdetection degrades to the existing 'claude-code' behavior, and deploying before the gateway accepts 'cowork' simply loses the new label without breaking hook correctness.

The detection logic is straightforward and well-tested across all three surfaces. The stop-event labeling gap is properly closed. No existing behavior is changed for non-Cowork sessions, and the path-fallback only fires when no env markers are present.

No files require special attention.

Important Files Changed

Filename Overview
claude-code/hooks/unbound.py Added _unbound_app_label(event) helper with layered Cowork detection; wired into all three call sites including the previously unlabeled stop-event exchange. Logic is correct and fail-open.
claude-code/hooks/test_pretool_mcp.py 8 new unit tests covering all detection branches (env flags, unrecognized entrypoints, path fallback, missing fields). setUp correctly isolates env state with clear=True.
binary/src/unbound_hook/init.py Version bump 0.1.8 → 0.1.9, consistent with packaging/unbound_discovery_entry.py.
packaging/unbound_discovery_entry.py Version bump 0.1.8 → 0.1.9, kept in lockstep with unbound_hook.version per the existing TODO comment.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Hook Event Received] --> B[_unbound_app_label]
    B --> C{CLAUDE_CODE_IS_COWORK == '1'?}
    C -- Yes --> D[return 'cowork']
    C -- No --> E{CLAUDE_CODE_ENTRYPOINT in local-agent / local_agent / remote_cowork?}
    E -- Yes --> D
    E -- No --> F{except Exception → pass}
    F --> G{cwd contains local-agent-mode-sessions?}
    G -- Yes --> D
    G -- No --> H{transcript_path contains local-agent-mode-sessions?}
    H -- Yes --> D
    H -- No --> I[return 'claude-code']
    D --> J[Set unbound_app_label on payload]
    I --> J
    J --> K{Which hook?}
    K -- process_pre_tool_use --> L[request_body.unbound_app_label]
    K -- process_user_prompt_submit --> M[request_body.unbound_app_label]
    K -- process_stop_event --> N[exchange.unbound_app_label]
    L --> O[send_to_api]
    M --> O
    N --> O
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[Hook Event Received] --> B[_unbound_app_label]
    B --> C{CLAUDE_CODE_IS_COWORK == '1'?}
    C -- Yes --> D[return 'cowork']
    C -- No --> E{CLAUDE_CODE_ENTRYPOINT in local-agent / local_agent / remote_cowork?}
    E -- Yes --> D
    E -- No --> F{except Exception → pass}
    F --> G{cwd contains local-agent-mode-sessions?}
    G -- Yes --> D
    G -- No --> H{transcript_path contains local-agent-mode-sessions?}
    H -- Yes --> D
    H -- No --> I[return 'claude-code']
    D --> J[Set unbound_app_label on payload]
    I --> J
    J --> K{Which hook?}
    K -- process_pre_tool_use --> L[request_body.unbound_app_label]
    K -- process_user_prompt_submit --> M[request_body.unbound_app_label]
    K -- process_stop_event --> N[exchange.unbound_app_label]
    L --> O[send_to_api]
    M --> O
    N --> O
Loading

Reviews (2): Last reviewed commit: "WEB-5030: bump unbound-hook + discovery ..." | Re-trigger Greptile

One hook script serves both Claude Code and Cowork. Detect Cowork via the
desktop env markers (CLAUDE_CODE_IS_COWORK=1 / CLAUDE_CODE_ENTRYPOINT in
local-agent|local_agent|remote_cowork) with the local-agent-mode-sessions
sandbox path (cwd/transcript_path) as a fallback for builds predating those
env vars. Send the label on pre_tool, user_prompt, and the end-of-turn
exchange (the exchange previously sent no app label). Falls back to
'claude-code'; fail-open.

Consolidates and supersedes #197 (adds the env-var + path detection, and the
end-of-turn exchange label that #197 omitted). Verified against real captured
events from CLI, desktop, and Cowork.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjaRWNPjy4N7KRQUykQape
Comment thread claude-code/hooks/unbound.py
Comment thread claude-code/hooks/test_pretool_mcp.py

@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.

Stale comment

Comment thread claude-code/hooks/unbound.py

@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)

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

Client-controlled unbound_app_label can skew gateway policy

Severity: MEDIUM · Confidence: 🔴 HIGH
Location: claude-code/hooks/unbound.py:1271
Impact: cowork vs claude-code on the pre-tool policy path is derived from process env (CLAUDE_CODE_IS_COWORK, CLAUDE_CODE_ENTRYPOINT) and a raw substring match on cwd/transcript_path; a user can influence those inputs (e.g. export env vars or work under a path containing local-agent-mode-sessions) and may receive laxer label-scoped policy if the gateway treats the label as authoritative.
Fix: Server-side, treat unbound_app_label as a hint—apply the stricter policy when labels diverge or corroborate with server-observable signals; client-side, prefer anchored path-segment matching over substring checks and document that env markers are authoritative only when set by the desktop host.
Flagged by: Cursor, Claude

Deploying hook before gateway support can silently weaken enforcement

Severity: MEDIUM · Confidence: 🟡 TRIAGE
Location: claude-code/hooks/unbound.py:1368
Impact: Cowork sessions previously evaluated under claude-code are now sent as cowork; older gateways drop unknown labels from label-keyed policy maps, so label-scoped controls stop matching with no client-visible error—a silent downgrade rather than a hard failure.
Fix: Roll out gateway support first (or gate the new label on a capability check); alternatively have the gateway map unknown labels to claude-code instead of dropping them.
Flagged by: Claude

Silent fail-open in label detection obscures misclassification

Severity: LOW · Confidence: 🔴 HIGH
Location: claude-code/hooks/unbound.py:1284
Impact: Env-marker reads are wrapped in a broad except Exception: pass and no branch logs which label was chosen; regressions (typo'd env name, unexpected errors) would misclassify Cowork as claude-code with no field trace, complicating policy-incident investigation.
Fix: Narrow the exception scope (or remove it if env reads cannot raise) and emit a debug-level log stating which branch produced the label.
Flagged by: Claude, Greptile


Clean scans: Gitleaks reported no secrets. Semgrep insecure-file-permissions hits at unbound.py:1783 and :2083 are outside this diff (pre-existing) and were not re-flagged.


🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 55d5e27e · 2026-07-03T10:03Z

pugazhendhi-m and others added 2 commits July 3, 2026 15:45
Add negative assertions that unrecognized CLAUDE_CODE_ENTRYPOINT values and a
non-'1' CLAUDE_CODE_IS_COWORK still resolve to 'claude-code', so a future
broadening of the Cowork membership check fails loudly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjaRWNPjy4N7KRQUykQape
Cut a new binary version so the cowork-aware hook ships in the signed macOS
runtime. hook (binary/src/unbound_hook/__init__.py) and discovery
(packaging/unbound_discovery_entry.py) versions stay in lockstep per the
release workflow's --version assert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RjaRWNPjy4N7KRQUykQape
@vigneshsubbiah16

Copy link
Copy Markdown
Collaborator

🛡️ Automated Security Review (consensus)

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

No actionable security issues in this diff. The change adds fail-open Cowork detection (_unbound_app_label) and wires it into three telemetry payloads; Gitleaks reported no secrets.

Previously acknowledged (not re-flagged)

  • Path/env-derived unbound_app_label spoofing@pugazhendhi-m: acknowledged, not a policy-enforcement bypass; path substring is a fallback behind authoritative env markers, and cowork/claude-code are enforced identically at the gateway (same native tools, budgets). Mislabel affects analytics attribution only, not security controls.
  • No debug logging in _unbound_app_label@pugazhendhi-m: intentionally silent on the PreToolUse hot path; label is visible in outbound payload/gateway telemetry. Greptile withdrew the suggestion.

🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 85d76493 · 2026-07-03T10:22Z

@pugazhendhi-m pugazhendhi-m merged commit ef93f58 into staging 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