[BUG] `claude -p` under macOS launchd takes ~143s per call (plugin autoupdate + async SessionStart hook wait before result); 3s from a terminal

Status Open
Reported on v2.1.267
Maintainer reply None cached
Activity 0 comments · opened Sep 20, 2026

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_KEY unset
  • 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 project added and WorkingDirectory set to an empty dir → rc=0 elapsed=2s, answer Ready.
  • --bare is not a workaround: it skips keychain reads, so it answers Not 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.

View original on GitHub ↗