.claude/skills/1password-cli-launchd-hang/SKILL.md
Fix 1Password CLI (op read) hanging indefinitely in macOS launchd/headless contexts, and fix incessant TCC "op would like to access data" popups on macOS Tahoe. Use when: (1) A LaunchAgent wrapper script calls `op read` and the service never starts, (2) `op read` works over SSH but hangs when run by launchd, (3) Setting OP_BIOMETRIC_UNLOCK_ENABLED=true or OP_SERVICE_ACCOUNT_TOKEN doesn't help under launchd, (4) Wrapper script appears to "exit silently" because op read blocks forever, (5) Repeated "op would like to access data from other apps" TCC popups on macOS Tahoe that don't persist after clicking Allow. Covers the cache-only pattern (recommended) and probe-timeout-cache pattern for reliable secret loading in headless services.
npx skillsauth add Dbochman/dotfiles 1password-cli-launchd-hangInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
op read (1Password CLI) hangs indefinitely when called from a macOS LaunchAgent, even with
OP_SERVICE_ACCOUNT_TOKEN set. The service account token works fine over SSH or in interactive
terminals, but under launchd the op process blocks forever — likely waiting for a Security
framework session or keychain access that launchd processes don't have.
This causes wrapper scripts that fetch secrets via op read to never reach the actual service
startup, making it look like the service "exits silently" (launchd reports exit code 0 from
the previous run while the current one hangs).
op readlaunchctl list but no process in psop read works fine when you SSH into the same machineOP_BIOMETRIC_UNLOCK_ENABLED=true makes it worse (waits for desktop app)OP_SERVICE_ACCOUNT_TOKEN doesn't fix it under launchd2>/dev/null on op read, masking the hangUse a probe-timeout-cache pattern:
op read succeeds)op worksop read for all secrets and refresh cache#!/bin/bash
export HOME="/Users/username"
export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin"
LOG="$HOME/.myservice/logs/wrapper.log"
echo "$(date -u +%FT%TZ) wrapper starting (pid=$$)" >> "$LOG"
# Load service account token
SA_TOKEN_FILE="$HOME/.myservice/.env-token"
if [[ -f "$SA_TOKEN_FILE" ]]; then
export OP_SERVICE_ACCOUNT_TOKEN=$(cat "$SA_TOKEN_FILE")
fi
CACHE_DIR="$HOME/.cache/myservice-secrets"
mkdir -p "$CACHE_DIR"
# Probe whether op read works (launchd blocks forever)
OP_AVAILABLE=false
if [[ -n "$OP_SERVICE_ACCOUNT_TOKEN" ]] && command -v op &>/dev/null; then
tmpfile=$(mktemp)
op read "op://MyVault/MySecret/password" > "$tmpfile" 2>/dev/null &
probe_pid=$!
i=0
while kill -0 "$probe_pid" 2>/dev/null && [[ $i -lt 3 ]]; do
sleep 1
i=$((i + 1))
done
if kill -0 "$probe_pid" 2>/dev/null; then
kill "$probe_pid" 2>/dev/null || true
echo "$(date -u +%FT%TZ) op read probe timed out - using cache" >> "$LOG"
else
wait "$probe_pid" 2>/dev/null || true
probe_val=$(cat "$tmpfile" 2>/dev/null || true)
if [[ -n "$probe_val" ]]; then
OP_AVAILABLE=true
printf "%s" "$probe_val" > "$CACHE_DIR/my_secret"
fi
fi
rm -f "$tmpfile"
fi
# Read secret: op if available, otherwise cache
_secret() {
local op_path="$1" cache_file="$2"
if [[ "$OP_AVAILABLE" == "true" ]]; then
local val
val=$(op read "$op_path" 2>/dev/null) || true
if [[ -n "$val" ]]; then
printf "%s" "$val" > "$cache_file"
printf "%s" "$val"
return
fi
fi
if [[ -f "$cache_file" ]]; then
cat "$cache_file"
fi
}
export MY_API_KEY=$(_secret "op://MyVault/API Key/password" "$CACHE_DIR/api_key")
exec /path/to/my/service
op read & + kill timer. Don't use $() subshell capture with
backgrounding — it doesn't reliably capture stdout from & processesop read output to a temp file, read it back after wait|| true everywhere: Prevent set -e from aborting on expected failures| Approach | Why It Fails |
|----------|-------------|
| OP_BIOMETRIC_UNLOCK_ENABLED=true | Desktop app can't prompt for biometric under launchd |
| OP_SERVICE_ACCOUNT_TOKEN alone | op CLI still needs some session context launchd lacks |
| perl -e 'alarm N; exec @ARGV' -- op read | op may ignore/block SIGALRM |
| timeout / gtimeout command | Not available on stock macOS |
| Subshell capture: val=$(op read ... &; wait $!) | Doesn't capture stdout from background process |
After deploying the wrapper:
# Clear logs and restart
launchctl unload ~/Library/LaunchAgents/my.service.plist
> ~/.myservice/logs/wrapper.log
launchctl load ~/Library/LaunchAgents/my.service.plist
sleep 8
# Check wrapper log for probe result
cat ~/.myservice/logs/wrapper.log
# Expected: "op read probe timed out - using cache" then "secrets loaded" then "exec-ing"
# Verify service is running
lsof -i :PORT
The 1Password desktop app registers a Mach bootstrap service on macOS. When the op CLI
starts, it spawns an op daemon --background process that connects to the desktop app via
this Mach port — this is a macOS-specific IPC mechanism that cannot be bypassed by environment
variables, --config flags, or socket manipulation.
Under launchd, the op daemon connects to the desktop app but the app requires user
interaction (Touch ID/GUI prompt) that can't happen in a non-GUI launchd context. The daemon
blocks waiting for the app to respond, and the CLI blocks waiting for the daemon.
| Approach | Result |
|----------|--------|
| OP_BIOMETRIC_UNLOCK_ENABLED=false | Still hangs — Mach port connection precedes env check |
| OP_SERVICE_ACCOUNT_TOKEN set | Still hangs — daemon spawns before token is evaluated |
| --config /isolated/dir | Still hangs — new daemon spawns in isolated dir, same behavior |
| unset SSH_AUTH_SOCK | Still hangs — not using SSH agent for IPC |
| XDG_CONFIG_HOME override | Still hangs — op doesn't use XDG for daemon |
| Disable "Integrate with CLI" in 1P settings | Still hangs — Mach service still registered |
| Kill op daemon + remove socket | New daemon auto-spawns on next op invocation |
| env -i minimal environment | Still hangs — Mach ports are per-user-session, not env-based |
The only reliable workarounds are:
op from the wrapper at all — read secrets exclusively from cache files. Refresh manually via SSH when needed.On macOS 26 (Tahoe), even the probe-timeout-cache pattern causes problems. The op CLI
triggers a "op would like to access data from other apps" TCC (Transparency, Consent, and
Control) popup every time it runs under launchd. Clicking "Allow" does NOT persist — the
permission resets on every gateway restart, causing an incessant popup storm.
op read N times = N popups per restartOn Tahoe, launchd-spawned processes get a transient TCC session. op CLI's data access
permission grant doesn't persist across process restarts because launchd creates a new
security session each time. This is different from pre-Tahoe behavior where TCC grants
for launchd services were sticky.
Remove ALL op calls from the gateway wrapper. Read secrets exclusively from a cache file.
Preferred pattern — single KEY=VALUE file sourced with set -a:
#!/bin/bash
export HOME="/Users/dbochman"
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin"
CACHE="$HOME/.openclaw/.secrets-cache"
# Source secrets from cache file (KEY=VALUE format, one per line).
if [[ -f "$CACHE" ]]; then
set -a
source "$CACHE"
set +a
else
echo "FATAL: No secrets cache at $CACHE" >&2
exit 1
fi
if [[ -z "${OPENCLAW_GATEWAY_TOKEN:-}" ]]; then
echo "FATAL: OPENCLAW_GATEWAY_TOKEN not found in cache" >&2
exit 1
fi
exec /path/to/node /path/to/openclaw/dist/entry.js gateway --port 18789
Cache file format (~/.openclaw/.secrets-cache, chmod 600):
OPENAI_API_KEY=sk-proj-...
ELEVENLABS_API_KEY=...
OPENCLAW_GATEWAY_TOKEN=...
BLUEBUBBLES_PASSWORD=...
Refresh helper (~/bin/openclaw-refresh-secrets):
#!/bin/bash
set -euo pipefail
export OP_SERVICE_ACCOUNT_TOKEN=$(cat "$HOME/.openclaw/.env-token")
CACHE="$HOME/.openclaw/.secrets-cache"
TMP=$(mktemp)
{
echo "OPENAI_API_KEY=$(op read 'op://OpenClaw/OpenAI API Key/password')"
echo "ELEVENLABS_API_KEY=$(op read 'op://OpenClaw/ElevenLabs API Key/password')"
echo "OPENCLAW_GATEWAY_TOKEN=$(op read 'op://OpenClaw/OpenClaw Gateway Token/password')"
} > "$TMP"
mv "$TMP" "$CACHE"
chmod 600 "$CACHE"
echo "Secrets cached to $CACHE"
Run over SSH when needed: ssh dylans-mac-mini ~/bin/openclaw-refresh-secrets
Why set -a; source over individual _cached() calls:
set -a auto-exports all sourced vars — no per-variable boilerplatecat ~/.openclaw/.secrets-cache shows all secretsImportant: Kill stale op daemon processes. Previous op read attempts each spawn an
op daemon --background process. These persist and cause TCC popup storms on Tahoe:
killall op 2>/dev/null # Clean up stale daemons after migrating to cache-only
chmod 600 on the cache directoryOP_SERVICE_ACCOUNT_TOKEN, run
op read manually to refresh cache files, then restart the serviceop vault list)op://VaultName/... not op://Private/... — service accounts typically can't
access the Private vaultset -e bash option interacts badly with background process patterns — avoid it or
use || true liberallylaunchd has a ThrottleInterval that delays restarts after crashes (default 10s).
Rapid crash-loop + throttle can make the service appear "dead" even with KeepAlive: trueop read with
OP_SERVICE_ACCOUNT_TOKEN works instantly under launchd — the hang only occurs when
the desktop app is present and its Mach bootstrap service is registereddevelopment
Search the web for current information, news, facts, and answers. Use when asked questions about current events, needing to look something up, finding websites, researching topics, or when you need up-to-date information beyond your training data.
development
Summarize any URL, YouTube video, podcast, PDF, or file into concise text. Use when asked to read an article, summarize a link, get the gist of a video or podcast, extract content from a URL, or when you need to understand what a web page or document contains.
development
Play music via Spotify and control Google Home speakers. Use when asked to play music, songs, artists, playlists, podcasts, or control speakers/volume/audio.
testing
Create new OpenClaw skills, modify and improve existing skills, and measure skill performance with evals. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy. Also use when asked to "make a skill", "turn this into a skill", "improve this skill", or "test this skill".