claude -p headless under non-TTY parent: parallel Task fan-out hangs + subagent path-scope ignores prompt scoping

Resolved 💬 1 comment Opened May 6, 2026 by jwizz13 Closed Jun 3, 2026

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's EnvironmentVariables block).
  • Not a missing-TTY issue. PTY emulation via script -q /dev/null allocates a real /dev/ttysNN for 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 asuser and via bootstrapped LaunchAgent

Happy to share the wrapper + skill files privately if useful for triage.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗