[object Object]

← 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 →