Skip to content

fix(hooks): classify Claude desktop-app sessions as the desktop voice channel - #2105

Open
pai-scaffolde wants to merge 1 commit into
danielmiessler:mainfrom
pai-scaffolde:fix/d-desktop-channel
Open

pai-scaffolde wants to merge 1 commit into
danielmiessler:mainfrom
pai-scaffolde:fix/d-desktop-channel

Conversation

@pai-scaffolde

Copy link
Copy Markdown

Reproduced on a fresh LifeOS 7.40.4 install (macOS, Claude Code in the Claude desktop app); the fix was applied to that install and run there before filing. Addresses the detector half left open on #1975 ("paste the CLAUDE* env from a desktop session and I'll wire it") — this PR is that answer.

Observed

Every session started from the Claude desktop app (Code tab) produced no voice; MEMORY/VOICE/voice-events.jsonl filled with event_type: "skipped", reason: "remote_channel:headless" for every notification.

Environment of a desktop-app session (measured in a tool-spawned child process; hook processes inherit the same env): CLAUDECODE=1, CLAUDE_CODE_CHILD_SESSION=1, CLAUDE_CODE_ENTRYPOINT=claude-desktop; TERM, TERM_PROGRAM, KITTY_WINDOW_ID, SSH_TTY all unset.

Root cause

hooks/lib/notification-channel.ts getNotificationChannel() infers "has a speaker" from terminal identity (TERM, TERM_PROGRAM, KITTY_WINDOW_ID, SSH_TTY). The desktop app is an interactive session with a speaker that sets none of them, so it fell into the launchd/cron headless branch.

Fix

Count CLAUDE_CODE_ENTRYPOINT === 'claude-desktop' as terminal identity. Rule 1 (explicit LIFEOS_NOTIFICATION_CHANNEL wins) is untouched and evaluated first, so the spawners that stamp children headless (Inference, CarrierProbe, PULSE lib, github-work; remote channels imessage/telegram/siri) are unaffected. CLAUDE_CODE_CHILD_SESSION is not used: it is set in the desktop main session too and is not a channel signal.

How tested

bun test hooks/lib/notification-channel.test.ts — 4 pass; on the shipped file the first case fails (entrypoint set, no terminal vars → headless, expected desktop). Cases: desktop marker alone → desktop; marker + explicit headless/imessage → the explicit value; nothing set or CLAUDE_CODE_ENTRYPOINT=cliheadless; TERM alone → desktop (unchanged).

Repro on a clean tree: from a desktop-app session, env -u TERM -u TERM_PROGRAM bun -e 'import {getNotificationChannel} from "./hooks/lib/notification-channel"; console.log(getNotificationChannel())' in ~/.claudeheadless before, desktop after; the next Stop hook plays. Voice is back on the live desktop-app install with this change.

Note on convention: this adds a bun:test file next to the lib (the repo has no runner today); happy to fold it into a probe table if that is preferred.

🤖 Generated with Claude Code

… channel

getNotificationChannel() infers "has a speaker" from terminal identity
(TERM, TERM_PROGRAM, KITTY_WINDOW_ID, SSH_TTY). The Claude desktop app is
an interactive session with a speaker that sets none of those; it sets
CLAUDE_CODE_ENTRYPOINT=claude-desktop. Every session started from the app
was therefore classified 'headless' and every voice-firing hook skipped
/notify with reason remote_channel:headless.

Count the desktop-app entrypoint as terminal identity. An explicit
LIFEOS_NOTIFICATION_CHANNEL still wins, so the spawners that stamp
children 'headless' are unaffected. Adds a bun test covering both
directions; the desktop case fails on the shipped file.

Public issue danielmiessler#1975.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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