[BUG] `claude -p` under macOS launchd takes ~143s per call (plugin autoupdate + async SessionStart hook wait before result); 3s from a terminal
Summary
claude -p (print mode, tools off, empty MCP map) takes ~143 s for a one-word answer when the process is started by macOS launchd, versus ~3 s for the identical command from a terminal. The model round trip is ~2 s in both cases; the rest is user-level settings work around the turn. Passing --setting-sources project from an empty directory (so no user hooks/plugins load) brings the launchd run down to 2 s and the keychain login still works.
The result text is not printed until this work finishes, so scheduled jobs that wrap claude -p in a subprocess timeout (ours: 300 s, real prompts ~25k tokens) time out every call while the same script works when run by hand.
Environment
- Claude Code 2.1.267 (native arm64 binary,
~/.local/bin/claude), macOS 25.6.0 (Darwin), Apple Silicon - Logged in with claude.ai (Max) OAuth;
ANTHROPIC_API_KEYunset - User settings enable several plugins (superpowers, security-guidance, i-have-adhd, impeccable, ui-design, last30days) and a handful of command hooks
Repro
Throwaway LaunchAgent (any label), RunAtLoad, running:
source "$HOME/.bash_profile"; unset ANTHROPIC_API_KEY
claude -p --debug-file /tmp/cc-launchd.debug.log --model sonnet --no-session-persistence \
--tools "" --strict-mcp-config --mcp-config '{"mcpServers":{}}' --output-format text \
--system-prompt "You answer in one word." "Say the word ready." </dev/null >/tmp/out.txt 2>&1
launchctl bootstrap gui/$(id -u) that.plist→rc=0 elapsed=143s- Same command line in a terminal (also with
env -i HOME=… PATH=…) →rc=0 elapsed=3–5s - Same LaunchAgent with
--setting-sources projectadded andWorkingDirectoryset to an empty dir →rc=0 elapsed=2s, answerReady. --bareis not a workaround: it skips keychain reads, so it answersNot logged in · Please run /login.
Debug log timeline (launchd run, gaps > 4 s)
GAP 5s after: File ~/.claude.json written atomically
next: [skills] failed to read ~/.claude/skills/<x>/SKILL.md: Error: EINTR: interrupted system call
GAP 5s after: installPluginsForHeadless: starting
GAP 29s after: "Hook Stop (Stop) success: …" (all Stop hooks finished here)
next: plugin autoupdate: checking installed plugins
GAP 17s after: Fetching manifest version for figma@claude-plugins-official: 2.2.111
next: Preserving file permissions … (~/.claude.json write)
GAP 63s after: [event-loop-stall] blocked for 610ms …
next: [engine] turn 1 end (turns=1 usage in=2 out=6 api=1948ms stop=end_turn resultLen=6)
then: "Hook SessionStart:startup (SessionStart) success: {"async": true, "asyncTimeout": 180000} …"
[uds-messaging] Shutting down … result printed, exit
api=1948ms for the actual request. The 63 s gap ends exactly when an async SessionStart hook (security-guidance's ensure_agent_sdk.py, which answers {"async": true, "asyncTimeout": 180000}) is logged as complete, so print mode appears to wait for async SessionStart hooks before emitting the result. The hook script itself finishes in <1 s when run directly under launchd, so the wait is not the hook's own runtime. In a second launchd run the same phase logged Lock acquisition took longer than expected - another Claude instance may be running (43 s) before the ~/.claude.json write.
Expected
In -p mode the result should be printed as soon as the turn ends; plugin auto-update and async SessionStart hooks should not sit between the model response and stdout. At minimum, a flag or env to skip plugin auto-update and async hook waits in print mode without giving up the keychain login (--bare currently bundles those together).
Workaround we shipped
--setting-sources project with cwd set to an empty temp dir, for every headless call made from a scheduled job.