← back to Terminal Status
fix: paint guard resolves ancestry before refusing on CLAUDE_CODE_CHILD_SESSION
914a88d17a623f4de661703861ce4eceb0a90fec · 2026-09-09 14:55:12 -0700 · Steve Abrams
The env var is set on any tool-spawned shell (Bash tool, hooks), so it made
/dot no-op for the real interactive session. Now ownership is resolved by
ancestry first — a top-level claude/codex that is the live owner of its tty
paints; only a genuine headless/nested agent (or no owning tty) is refused.
All 26 tests pass incl. test_headless_nested_agent_cannot_paint_parent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzZkMwx4e9ec829p24cAp3
Files touched
Diff
commit 914a88d17a623f4de661703861ce4eceb0a90fec
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 9 14:55:12 2026 -0700
fix: paint guard resolves ancestry before refusing on CLAUDE_CODE_CHILD_SESSION
The env var is set on any tool-spawned shell (Bash tool, hooks), so it made
/dot no-op for the real interactive session. Now ownership is resolved by
ancestry first — a top-level claude/codex that is the live owner of its tty
paints; only a genuine headless/nested agent (or no owning tty) is refused.
All 26 tests pass incl. test_headless_nested_agent_cannot_paint_parent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VzZkMwx4e9ec829p24cAp3
---
terminal_status.py | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
diff --git a/terminal_status.py b/terminal_status.py
index 32282df..8cc6e20 100644
--- a/terminal_status.py
+++ b/terminal_status.py
@@ -134,8 +134,13 @@ def owners(rows):
def current_owner(rows):
- if os.environ.get("CLAUDE_CODE_CHILD_SESSION"):
- raise StatusError("A subagent has no terminal of its own; refusing to paint")
+ # Resolve ownership by ancestry FIRST. CLAUDE_CODE_CHILD_SESSION is set on
+ # any tool-spawned shell (the Bash tool, hooks), including a legit top-level
+ # session's own subshell, so it can't be the sole refusal signal — that made
+ # /dot no-op for the real interactive session. The reliable test is whether
+ # this process's ancestry resolves to a `claude`/`codex` that is the LIVE
+ # OWNER of its tty (owners() already excludes nested agents sharing a parent
+ # terminal); if so, that tty IS ours to paint. This mirrors topic-title.sh.
live = owners(rows)
for p in ancestors(os.getpid(), rows):
if p.runtime:
@@ -143,6 +148,10 @@ def current_owner(rows):
if live.get(p.tty) == p.owner():
return p.owner()
raise StatusError("Headless or nested agent; refusing parent-terminal write")
+ # No owning runtime in ancestry at all: only now does the child-session flag
+ # decide — a true background subagent has no terminal of its own.
+ if os.environ.get("CLAUDE_CODE_CHILD_SESSION"):
+ raise StatusError("A subagent has no terminal of its own; refusing to paint")
raise StatusError("No owning Claude/Codex terminal in this process ancestry")
← 418fa09 snapshot before child-session paint-guard fix
·
back to Terminal Status
·
fix: raise process-table timeout so dot painting survives a 63f3fae →