claude -p headless under non-TTY parent: parallel Task fan-out hangs + subagent path-scope ignores prompt scoping
Summary
When claude -p is invoked from a process with no controlling terminal (cron, launchctl asuser, bootstrapped LaunchAgent, even with script -q /dev/null PTY emulation), two distinct misbehaviors surface that don't appear under interactive use. Both block headless automation patterns (cron-fired audits, scheduled reports) that work fine when run by hand.
This report bundles both because they share a common context (non-TTY parent) and triage may benefit from seeing the pattern; happy to split into separate issues if preferred.
---
Bug 1 — Parallel Task-tool fan-out hangs indefinitely
Symptom. When the model issues a single assistant message containing 5 parallel Task tool calls (typical pattern when a skill dispatches per-zone explorer subagents), the parent claude process consumes CPU but produces zero stdout for 10+ minutes. Single-Task dispatch from the same non-TTY parent works fine in <30s.
Reproducible under at least two non-interactive contexts (launchctl asuser and bootstrapped launchd plist), and survives PTY emulation via script -q /dev/null (the parent claude has a real /dev/ttysNN allocated by script(1) — verified via ps -ef — and still hangs).
Cheap repro that DOES reproduce (any custom skill that dispatches multiple Task subagents in one assistant message):
launchctl asuser $(id -u) script -q /dev/null \
claude -p --permission-mode bypassPermissions \
"/<skill-that-fans-out-5-parallel-Task-calls>"
Cheap micro-test that does NOT reproduce (single subagent works):
launchctl asuser $(id -u) claude -p --permission-mode bypassPermissions \
"Use the explorer agent to print all *.py paths in <some-dir>. Do not use any other tools."
Evidence:
| Test | Mode | Result |
|---|---|---|
| Cheap "hello" prompt (no tool calls) | launchctl asuser | 4.2s ✓ (vs 38.7s interactive) |
| Single explorer subagent (one Task call) | launchctl asuser | 29s ✓ |
| Single explorer subagent (one Task call) | baseline (no asuser) | 42s ✓ |
| Skill with 5 parallel Task calls in one message | launchctl asuser + script -q /dev/null PTY | hung 10+ min, zero stdout, killed |
| Skill with 5 parallel Task calls in one message | bootstrapped launchd plist | hung 13+ min, zero stdout, killed |
Process tree during the hang (from ps -ef):
script -q /dev/null bash wrapper.sh (pid 11458)
bash wrapper.sh (pid 11460, ttys006)
claude -p ... <skill prompt> (pid 11485, ttys006, 17s CPU consumed at 10-min mark)
What this rules out:
- Not a launchd-env-block issue. Hangs reproduce under
launchctl asuser(which inherits caller's env) the same as bootstrapped launchd (which uses only the plist'sEnvironmentVariablesblock). - Not a missing-TTY issue. PTY emulation via
script -q /dev/nullallocates a real/dev/ttysNNfor the entire process tree. - Not a permission-mode issue. All tests use
--permission-mode bypassPermissions. Single-Task tests under this flag work fine. - Not stdout buffering alone. Buffering would delay output but not consume 17s of CPU with zero output. The parent is doing work; it just isn't producing observable progress.
Suspected cause. Likely a Task-tool subagent dispatch IPC issue under non-interactive parent processes — possibly stdin/stdout pipe semantics differing between interactive and headless claude -p parents, or a race in subagent process spawning under no-TTY contexts.
Workaround we shipped. Instructed our skill's cron-fired prompt to dispatch the subagents serially (5 separate Task calls, one at a time). Adds ~2 min to wall time but avoids the hang.
---
Bug 2 — Subagent path-scope ignores prompt scoping under bootstrapped launchd
Symptom. When a bootstrapped LaunchAgent (Claude Code 2.1.129) fired a custom skill scoped via prompt to specific project repos, macOS surfaced TCC permission prompts requesting access to:
~/Desktop- Photos library / Media
~/Library/CloudStorage/GoogleDrive-*(Google Drive sync paths)
The skill's brief explicitly listed three SEARCH ROOTS for the explorer subagents. The subagents should have been searching only those paths. Instead they recursed into TCC-protected user directories, surfacing the permission prompts.
Why this is bad. Headless launchd has no GUI session to grant TCC. A prompt under bootstrapped launchd hangs the run waiting for user input that never arrives (separate failure mode from Bug 1 above).
Version observation. The same skill behaves differently across CLI versions:
| Mode | claude-version | TCC behavior |
|---|---|---|
| launchctl asuser (inherits caller's PATH) | 2.1.128 | No TCC prompts (skill output reported only project-path findings) |
| Bootstrapped LaunchAgent (plist EnvironmentVariables.PATH) | 2.1.129 | TCC prompts during execution |
Could be a 2.1.128 → 2.1.129 regression in subagent path-scoping defaults, or coincidence of model behavior under different env contexts. We don't have enough samples to be sure.
What we'd want. Subagents (Task tool dispatches) should default to prompt-derived scope only under non-interactive claude -p parents. Touching paths outside the explicit prompt or skill brief should require a confirmation prompt that's never automatically granted under bypassPermissions. For TCC-protected paths specifically (Desktop, Documents, Pictures, Movies, Music, Downloads, CloudStorage), a hard deny would be safer than a prompt — prompts in headless contexts hang waiting for input that never arrives.
Workaround we shipped. Hardcoded a deny list of TCC-protected paths into our skill's brief template, with explicit "DO NOT recurse" instructions and a copyable find -path ... -prune example. This relies on the LLM respecting the brief instruction, which is the same trust surface that originally failed — it's a workaround, not a fix.
---
Why this matters
Cron and LaunchAgent users hit both bugs in normal automation flows (scheduled audits, weekly reports, periodic checks). Both have working workarounds, so this is not blocking us — but the workarounds (serial dispatch + prompt-level deny lists) are skill-author burden, not harness defaults. Anybody else writing headless claude -p automation will hit the same surface.
Environment
- macOS 25.3.0 (Darwin)
- Claude Code CLI 2.1.128 and 2.1.129 (both reproduce Bug 1; Bug 2 observed only on 2.1.129)
- Shell:
/bin/zsh - Repro context: weekly cron-fired audit script invoked from a bash wrapper under
launchctl asuserand via bootstrapped LaunchAgent
Happy to share the wrapper + skill files privately if useful for triage.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗