Skip to content

fix(hook): treat a shell-wrapped claude as primary, not nested - #135

Open
paskal wants to merge 1 commit into
alexei-led:mainfrom
paskal:fix/nested-guard-non-leader-claude
Open

fix(hook): treat a shell-wrapped claude as primary, not nested#135
paskal wants to merge 1 commit into
alexei-led:mainfrom
paskal:fix/nested-guard-non-leader-claude

Conversation

@paskal

@paskal paskal commented Jul 28, 2026

Copy link
Copy Markdown

Hi there, thanks a lot for ccgram, the tmux-side-rather-than-SDK-side approach is exactly what I wanted.## Problem_is_nested_session treats a claude as primary only when its PID equals the foreground process group id on the pane's tty:python owner = _closest_claude_ancestor(snapshot, os.getpid()) if owner is None: return False return owner != fg_pgid That holds when a shell execs claude directly, but not when a launcher starts the agent as bash -lc "... && claude ...". The shell stays group leader, so the primary claude is its child and its PID never equals the foreground PGID. On such a pane:fg pgid = 14053 /bin/bash claude = 14058 (pgid 14053)Every hook that session fires is then classified as a nested observer and dropped. It fails silently, so session_map.json is never written and Telegram forwarding never starts for that window. ccgram doctor reports all hooks installed and the topic binds fine, which makes it look like a transcript problem rather than a discarded hook.This is how agent-deck launches sessions, so ccgram adopts its windows as topics and then stays silent in all of them.## ChangeDecide nesting by asking whether another claude sits above this one in the process tree, rather than whether it happens to lead the process group. The fast path for a group-leading primary is kept, and every fails-open branch is untouched.This preserves #62, which the guard was written for. It is arguably a closer test of that issue's intent than the pgid comparison, since a nested claude inherits the parent's pgid rather than getting its own, as the original comment notes, so PID vs PGID cannot reliably separate the two shapes on its own.Two cases added to TestNestedSessionDetection: a shell-wrapped primary is not nested, and an observer under a shell-wrapped primary still is. The existing cases in that class and test_nested_session_start_does_not_steal_forwarding pass unchanged.Verified on a real agent-deck session, where hooks were dropped before the change and bind correctly after it, with session_map.json getting the right session id, transcript path and provider_name: claude.

_is_nested_session decided a claude was primary only when its PID equalled the
foreground process group id on the pane's tty. That holds when a shell execs
claude directly, but not when a launcher starts the agent as
`bash -lc "... && claude ..."`: the shell stays group leader, so the primary
claude is its child and its PID never equals the foreground PGID. Every hook
such a session fires is then dropped as a nested observer, silently, so
session_map.json is never written and Telegram forwarding never starts.

Decide nesting by asking whether another claude sits above this one in the
process tree instead. That covers both primary shapes and is what the pgid
comparison was approximating anyway, since a nested claude inherits the same
pgid rather than getting its own (as the original comment noted).

The fast path for a group-leading primary is kept, and every fails-open branch
is untouched. Verified against a real agent-deck session, which launches claude
through a login shell: hooks were dropped before the change and bind correctly
after it.
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