Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions claude-code/hooks/unbound.py
Original file line number Diff line number Diff line change
Expand Up @@ -1178,6 +1178,24 @@ def build_account_identity(probe: bool = False) -> Dict:
return identity


def _unbound_app_label() -> str:
"""Cowork sessions run the same CLI + hooks as Claude Code; the desktop
app marks the surface in the hook environment. Report them under their
own label so the gateway can scope policies/analytics per surface.
Requires gateway support for 'cowork' (ai-gateway#716) — old
gateways would drop the label out of their label-keyed maps."""
try:
if os.environ.get('CLAUDE_CODE_IS_COWORK') == '1':
return 'cowork'
if os.environ.get('CLAUDE_CODE_ENTRYPOINT') in (
'local-agent', 'local_agent', 'remote_cowork'
):
return 'cowork'
except Exception:
pass
return 'claude-code'
Comment on lines +1187 to +1196

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 IS_COWORK=0 explicit denial overridden by entrypoint match

When CLAUDE_CODE_IS_COWORK=0 is set explicitly (meaning "this is not a Cowork session") but CLAUDE_CODE_ENTRYPOINT is simultaneously set to local-agent or remote_cowork, the function skips the IS_COWORK check (it's not '1') and then falls through to the entrypoint check, returning 'claude-cowork' anyway. The explicit negative signal is effectively ignored. The entrypoint fallback should only fire when CLAUDE_CODE_IS_COWORK is absent (None), not when it is explicitly set to '0'. If the desktop app ever sets both variables in a non-Cowork context, sessions will be silently mislabeled and routed against wrong gateway policy maps.

Comment on lines +1187 to +1196

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 Silent exception swallow with no logging

os.environ.get() never raises in practice, so this except Exception: pass only matters if something truly unexpected happens. When it does fire, there is no log, no metric, and no way to reconstruct in production why a session was silently downgraded to the 'claude-code' label. Per the team's logging guidelines, caught exceptions should be logged with context rather than silently dropped.

Suggested change
try:
if os.environ.get('CLAUDE_CODE_IS_COWORK') == '1':
return 'claude-cowork'
if os.environ.get('CLAUDE_CODE_ENTRYPOINT') in (
'local-agent', 'local_agent', 'remote_cowork'
):
return 'claude-cowork'
except Exception:
pass
return 'claude-code'
try:
if os.environ.get('CLAUDE_CODE_IS_COWORK') == '1':
return 'claude-cowork'
if os.environ.get('CLAUDE_CODE_ENTRYPOINT') in (
'local-agent', 'local_agent', 'remote_cowork'
):
return 'claude-cowork'
except Exception as exc: # pragma: no cover
import logging as _logging
_logging.getLogger(__name__).warning('_unbound_app_label error, defaulting to claude-code: %s', exc)
return 'claude-code'



def process_pre_tool_use(event: Dict, api_key: str) -> Dict:
"""Process PreToolUse event - DO NOT LOG."""
session_id = event.get('session_id')
Expand Down Expand Up @@ -1248,7 +1266,7 @@ def process_pre_tool_use(event: Dict, api_key: str) -> Dict:

request_body = {
'conversation_id': session_id,
'unbound_app_label': 'claude-code',
'unbound_app_label': _unbound_app_label(),
Comment thread
cursor[bot] marked this conversation as resolved.
'model': model,
'event_name': 'tool_use',
'pre_tool_use_data': {
Expand Down Expand Up @@ -1343,7 +1361,7 @@ def process_user_prompt_submit(event: Dict, api_key: str) -> Dict:

request_body = {
'conversation_id': session_id,
'unbound_app_label': 'claude-code',
'unbound_app_label': _unbound_app_label(),
'model': model,
'event_name': 'user_prompt',
'account_identity': build_account_identity(),
Expand Down