diff --git a/CLAUDE.md b/CLAUDE.md
index 6e1f3b37b1..0b7cd94879 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -946,7 +946,15 @@ explicitly makes it read `~/.claude/.claude.json`, a file nothing else
uses. `connect` therefore addresses the default directory by *unsetting*
the variable. And launchd gives an agent 256 open files by default; the
daemon exhausted that within an hour and fell back to archive snapshots for
-inbox reads, so the plist sets 4096.
+inbox reads, so the plist sets 4096. That only buys time: `am` 0.3.36 leaks
+sqlite handles on its store (234 of 258 after five days, first seen
+2026-09-29), and a plist written before the limit existed keeps launchd's
+256 until `postbox install` rewrites it. `status` therefore prints a `files:`
+line — open handles against the *installed* plist's limit, naming a stale
+plist and warning past 80%. `install` waits for `bootout` to finish before
+`bootstrap`: launchd tears the job down after bootout returns, and a
+bootstrap in that window fails "5: Input/output error" and leaves nothing
+loaded.
Commands: `name [--source] [dir]`, `names [root...]`, `install`, `uninstall
[--purge]`, `start|stop|restart|status|logs`, `connect|disconnect`, `hook
diff --git a/bin/executable_postbox b/bin/executable_postbox
index 0b140fc9be..95ea6b9bdf 100755
--- a/bin/executable_postbox
+++ b/bin/executable_postbox
@@ -416,6 +416,12 @@ cmd_install() {
mkdir -p "$HOME/Library/LaunchAgents"
write_plist
launchctl bootout "gui/$(uid)/$LABEL" >/dev/null 2>&1
+ # bootout returns before launchd has torn the job down; a bootstrap in
+ # that window fails "5: Input/output error" and leaves nothing loaded.
+ i=0
+ while launchctl print "gui/$(uid)/$LABEL" >/dev/null 2>&1 && [ $i -lt 50 ]; do
+ sleep 0.1; i=$((i + 1))
+ done
launchctl bootstrap "gui/$(uid)" "$(plist_path)" || die "launchctl bootstrap failed (see $(plist_path))"
launchctl kickstart -k "gui/$(uid)/$LABEL" >/dev/null 2>&1
printf 'postbox: daemon registered as %s on %s\n' "$LABEL" "$(url)"
@@ -887,6 +893,17 @@ cmd_status() {
if launchctl print "gui/$(uid)/$LABEL" >/dev/null 2>&1; then
pid=$(launchctl print "gui/$(uid)/$LABEL" 2>/dev/null | awk '/^\tpid = / { print $3; exit }')
printf 'daemon: %s loaded%s\n' "$LABEL" "${pid:+ (pid $pid)}"
+ # am leaks sqlite handles (~1 per half hour on 0.3.36), so the
+ # headroom is worth watching. The limit is what the INSTALLED plist
+ # says — a plist from before the limit existed runs on launchd's 256.
+ if [ -n "$pid" ]; then
+ open=$(lsof -p "$pid" 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')
+ lim=$(grep -A3 'SoftResourceLimits' "$(plist_path)" 2>/dev/null | sed -n 's:.*\([0-9]*\).*:\1:p')
+ printf 'files: %s open of %s' "$open" "${lim:-256}"
+ [ -z "$lim" ] && printf ' (launchd default: the installed plist predates the limit — postbox install)'
+ [ "$open" -ge $(( ${lim:-256} * 4 / 5 )) ] && printf ' — WARNING: over 80%%; postbox restart'
+ printf '\n'
+ fi
else
printf 'daemon: %s not loaded (postbox install)\n' "$LABEL"
fi
diff --git a/tests/postbox.bats b/tests/postbox.bats
index f12987fe77..6d700c5410 100644
--- a/tests/postbox.bats
+++ b/tests/postbox.bats
@@ -343,6 +343,48 @@ STUB
[[ "$output" == *"NOT answering"* ]]
}
+@test "install: waits for bootout to finish before bootstrapping again" {
+ # launchd keeps the old job for a moment after bootout returns; a
+ # bootstrap in that window fails with error 5 and leaves nothing loaded.
+ echo 3 > "$BATS_TEST_TMPDIR/still-loaded"
+ cat > "$BIN/launchctl" <<'STUB'
+#!/bin/sh
+printf '%s\n' "$*" >> "$LAUNCHCTL_LOG"
+f="$BATS_TEST_TMPDIR/still-loaded"
+if [ "$1" = print ]; then
+ n=$(cat "$f"); [ "$n" -gt 0 ] || exit 1
+ echo $((n - 1)) > "$f"; exit 0
+fi
+exit 0
+STUB
+ run "$PB" install
+ [ "$status" -eq 0 ]
+ # bootout, three prints that still see the job, the one that does not, then bootstrap.
+ [ "$(grep -o '^[a-z]*' "$LAUNCHCTL_LOG" | tr '\n' ' ')" = "bootout print print print print bootstrap kickstart " ]
+}
+
+@test "status: open files against the installed plist's limit, and a stale plist is named" {
+ cat > "$BIN/launchctl" <<'STUB'
+#!/bin/sh
+[ "$1" = print ] && printf '\tpid = 4242\n'
+exit 0
+STUB
+ cat > "$BIN/lsof" <<'STUB'
+#!/bin/sh
+echo HEADER; i=0; while [ $i -lt "$FAKE_FDS" ]; do echo fd; i=$((i + 1)); done
+STUB
+ chmod +x "$BIN/launchctl" "$BIN/lsof"
+ "$PB" install >/dev/null
+ FAKE_FDS=40 run "$PB" status
+ [[ "$output" == *"files: 40 open of 4096"$'\n'* ]]
+ FAKE_FDS=3300 run "$PB" status
+ [[ "$output" == *"files: 3300 open of 4096 — WARNING: over 80%; postbox restart"* ]]
+ # A plist written before the limit existed runs on launchd's default.
+ sed -i.bak '/ResourceLimits/,/<\/dict>/d' "$HOME/Library/LaunchAgents/local.postbox.plist"
+ FAKE_FDS=261 run "$PB" status
+ [[ "$output" == *"files: 261 open of 256 (launchd default: the installed plist predates the limit — postbox install) — WARNING"* ]]
+}
+
@test "status: an installed version other than the pinned one is a nudge to re-check the vocabulary" {
cat > "$BIN/am" <<'STUB'
#!/bin/sh