Proof Portal

Project overview

Omarchy

ProbeLabs65 findings · 95 requirements

A proof layer — requirements, tests and verified fixes — for two of Omarchy's subsystems: the application menu (launcher scripts, QML model, JSONC config, search and selection) and the lock screen (lock scripts, QML, PAM authentication). Scope is deliberately limited to those components of omacom/omarchy; the rest of the distribution is not covered.

Inbox
ReportR-4482GitHub issuePublicReceived

Waybar reloads leak `voxtype status --follow` orphans despite `trap 'kill 0' EXIT` (#5345 didn't fix it)

Reported by Fatih0234 · filed Jul 5, 2026Closed upstream on 2026-07-18 by dhh. GitHub close reason: completed. No closing PR or commit is recorded — "completed" alone is not evidence of a fix.

Received and recorded verbatim. Nothing has been checked yet.

At a glance

1 · What is claimed?
Only the report text below — no validator has restated it as checkable claims yet.
2 · What does it affect?
Not yet mapped to any requirement or component.
3 · Is it reproducible?
No validation has been run yet.
4 · What remains unclear?
  • Not validated yet: no claims, no reproducer.
5 · What happens next?
Our staff decide whether the report is valid. Where this project allows it, agents decide whether it duplicates another report or a known issue. No decision has been recorded yet.

Original report

Waybar reloads leak `voxtype status --follow` orphans despite `trap 'kill 0' EXIT` (#5345 didn't fix it)revision 1 · captured 2026-10-04 11:56 UTC via poll

TL;DR

bin/omarchy-voxtype-status still leaks one orphaned bash + one orphaned voxtype status --follow process per Waybar reload, despite the trap 'kill 0' EXIT added in #5345. The EXIT trap only runs after the wrapper bash actually exits — and the wrapper is parked in a blocking read, so it never exits and the trap never fires. The fix needs an active parent-death watcher plus a coproc-based PID-tracked kill. A working patch + reproduction tests are included below.

Symptom (observed in the wild)

After ~1d21h of normal desktop use with omarchy + voxtype + Waybar, the process table showed 10 orphaned `voxtype status --follow` families (10 voxtype + 11 bash/wrapper procs, ~110 MB RSS combined, all idle cpu 0.0%):

$ pgrep -af 'voxtype status --follow' | grep -v grep | wc -l
10

$ ps -eo pid,ppid,pcpu,rss,etime,cmd | grep -E 'voxtype status --follow|omarchy-voxtype-status' | grep -v grep
1662   ppid=993  cpu=0.0 rss=3428  etime=1-21:23:16 voxtype status --follow --extended --format json
1664   ppid=993  cpu=0.0 rss=4008  etime=1-21:23:16 /bin/bash /home/…/omarchy-voxtype-status
96944  ppid=993  cpu=0.0 rss=5396  etime=1-03:48:50 voxtype status --follow --extended --format json
… (8 more pairs, all re-parented to PID 993 = systemd --user)

PID 993 is systemd --user — i.e. Waybar died/reloaded, the spawn was orphaned, and Waybar never sent any signal to the wrapper. The voxtype --follow child is left blocking until systemd decides otherwise.

Why #5345 (trap 'kill 0' EXIT) isn't effective

trap … EXIT fires only when the bash process is about to exit. The current script's loop:

voxtype status --follow --extended --format json | while read -r line; do
  echo "$line" | jq -c '. + {alt: .class}'
done

…blocks forever inside while read. With --follow, voxtype emits a line on state change (idle → recording → transcribing → idle) and otherwise pauses. The wrapper bash is parked in the read syscall. There is no condition under which the wrapper exits on its own in the parent-death case, and Waybar does not always signal it, so:

  • trap 'kill 0' EXIT → never runs.
  • kill 0 (a process-group SIGTERM) → never sent.
  • voxtype shim → never killed.
  • bash wrapper + child voxtype → leak, one per reload.

Even in the rare case Waybar does signal the wrapper (SIGTERM/SIGHUP), the existing EXIT trap's kill 0 blurs into the entire process group the wrapper happens to share with whatever spawned it (Waybar itself, the bar's other helpers, callers that exec this script directly) — not what you want. Verified empirically: when I tested the OLD script without isolating it, its kill 0 killed my test runner.

Reproduction

Below is a self-contained test harness. It mimics Waybar's reload-on-exit path: a parent subshell spawns the wrapper then exits (no signal), exactly like Waybar dying. Re-run produces the consistent result below.

shims/voxtype is a fake voxtype that emits one line then exec sleep 3600 (like a real --follow once it has emitted its initial status):

mkdir -p /tmp/wt/shims && cd /tmp/wt
cat > shims/voxtype <<'VOXT'
#!/bin/bash
echo $$ > "$VT_PIDFILE"
echo '{"class":"idle","text":"","tooltip":"ready","model":"base.en","device":"default","backend":"cpu"}'
exec sleep 3600
VOXT
cat > shims/omarchy-cmd-present <<'CMD'
#!/bin/bash
for c in "$@"; do command -v "$c" >/dev/null 2>&1 || exit 1; done
exit 0
CMD
chmod +x shims/*

cat > omarchy-voxtype-status.old <<'OLD'        # current omarchy tree
#!/bin/bash
trap 'kill 0' EXIT
if ! omarchy-cmd-present voxtype 2>/dev/null; then
  echo '{"alt": "", "tooltip": ""}'; exit 0
fi
voxtype status --follow --extended --format json | while read -r line; do
  echo "$line" | jq -c '. + {alt: .class}'
done
OLD

# Proposed fixed version is "omarchy-voxtype-status.final" (shown later)
chmod +x omarchy-voxtype-status.old

alive(){ if [ -n "$1" ] && kill -0 "$1" 2>/dev/null; then echo LEAKED; else echo REAPED; fi; }
test_variant(){ local v=$1; pf=VT_$v.pid; rm -f $pf
  (
    export PATH="$PWD/shims:$PATH" VT_PIDFILE="$PWD/$pf"
    setsid bash "$PWD/omarchy-voxtype-status.$v" >/dev/null 2>&1 &
    sleep 3
  ) &
  wait $!; sleep 4
  vp=$(cat $pf 2>/dev/null)
  wp=$(pgrep -f "omarchy-voxtype-status.$v" | grep -v $$ | head -1)
  echo "  $v voxtype child $(alive $vp)"
  echo "  $v wrapperbash   $(alive $wp)"
  kill -9 $vp $wp 2>/dev/null
}

echo "Parent-exits-without-signal scenario:"
test_variant old
test_variant final

Result on this end (consistent across runs):

Parent-exits-without-signal scenario:
  old voxtype child LEAKED
  old wrapperbash   LEAKED
  final voxtype child REAPED
  final wrapperbash   REAPED

For completeness I also verified the cases where Waybar does signal the wrapper:

  • SIGTERM to wrapper → both OLD and FINAL reap (OLD's EXIT trap does fire here).
  • SIGHUP to wrapper → FINAL reaps both. (Stale Old has no HUP trap, but Waybar doesn't use HUP.)
  • voxtype bin genuinely missing → FINAL emits the static {"alt":"","tooltip":""} and exits cleanly with no background watcher/voxtype spawned.
  • CPU at idle → with the fix in place, both the wrapper (cpu 0.0%) and the watcher subshell poll-loop (cpu 0.0%) sit idle for arbitrary uptime; the polling watcher does not become a non-zero CPU load.
  • 5 consecutive reload cycles (parent-exit then respawn) → 0 cumulative leaks.
  • Watchtower: no zombies — cleanup reaps the watcher subshell and the voxtype coproc via wait, leaving no <defunct> entries.

A second non-obvious WIN from the fix: it also kills the OLD kill 0 blast radius — the new code kills the voxtype PID directly (kill -- "$VPID") rather than relying on kill 0, which was unsafe when the wrapper shared a process group with its spawner.

Proposed fix

Drop-in replacement for bin/omarchy-voxtype-status:

#!/bin/bash

# omarchy:summary=Clean up the voxtype --follow child when Waybar reloads

# Background
# ----------
# Waybar's custom/voxtype module runs this script via `exec`. The script
# spawns a long-lived `voxtype status --follow` child. When Waybar reloads
# (config change, display hotplug, Hyprland reload, or Waybar itself
# exits/restarts) the wrapper is NOT reliably signalled — on the Waybar-exit
# path Waybar just dies, leaving the wrapper re-parented to systemd --user.
# The wrapper is parked inside a blocking `read`, so it never exits on its
# own, and the previous `trap 'kill 0' EXIT` only fires *after* the wrapper
# exits — which never happens. Result: one orphaned bash + one orphaned
# voxtype --follow per reload. (Verified empirically: 10 orphan pairs after
# ~1d21h uptime, long after PR #5345's `trap 'kill 0' EXIT` merged.)
#
# Robust strategy
# ---------------
# Two independent teardown paths; either alone suffices.
#
#   1. Parent-death watcher (closes the leak EXIT-trap alone couldn't).
#      A background subshell polls $PPID every second; when Waybar (the
#      original parent at this script's startup) is gone, it sends SIGTERM
#      to this shell, firing the cleanup trap. $PPID is a readonly bash
#      special parameter set once at startup — it does NOT change after
#      re-parenting, so the watcher reliably detects the original parent's
#      death even though by then a new parent has taken over.
#
#   2. Signal traps (for cases where Waybar DOES signal us). SIGTERM and
#      SIGHUP fire cleanly while blocked inside `read`; both run cleanup.
#      (SIGINT does not fire reliably while inside `read` from a non-tty
#      pipe fd — we trap it anyway for any non-`read` context. SIGKILL is,
#      of course, uncatchable — path 1 covers that case.)
#
# Cleanup explicitly kills the watcher, the voxtype --follow child, and
# reaps the zombies via `wait`. We track the voxtype PID directly via
# `coproc` ($COPROC_PID) instead of relying on `kill 0` process-group
# semantics — direct PID kill is portable regardless of job-control state.

VPID=""
WPID=""

cleanup() {
  trap '' INT TERM HUP EXIT        # prevent re-entry
  [ -n "$WPID" ] && kill -- "$WPID" 2>/dev/null
  [ -n "$VPID" ] && kill -- "$VPID" 2>/dev/null
  wait -- "$WPID" "$VPID" 2>/dev/null   # reap, leave no zombies
  exit 0
}
trap cleanup INT TERM HUP EXIT

if ! omarchy-cmd-present voxtype 2>/dev/null; then
  # voxtype not installed: emit one-shot empty status, exit cleanly.
  echo '{"alt": "", "tooltip": ""}'
  exit 0
fi

# Path 1: parent-death watcher.
( while kill -0 "$PPID" 2>/dev/null; do sleep 1; done
  kill -TERM "$$" 2>/dev/null ) &
WPID=$!

# Path 2: voxtype --follow as a coproc, PID tracked explicitly.
coproc voxtype status --follow --extended --format json
VPID=$COPROC_PID

while read -r line; do
  printf '%s\n' "$line" | jq -c '. + {alt: .class}'
done <&${COPROC[0]}

Net change vs bin/omarchy-voxtype-status on dev

  • Removed: the bare trap 'kill 0' EXIT (ineffective in the parent-death case, and risky when the wrapper shares its pgroup with its spawner).
  • Added: explicit cleanup() running under trap … INT TERM HUP EXIT that kills the watcher subshell and the voxtype child by PID, then waits both so no zombies are left.
  • Added: a parent-death watcher subshell that polls the original $PPID once per second and sends SIGTERM to this shell when Waybar is gone. This is the path that closes the leak Waybar's exit-without-signalling leaves open.
  • Switched the voxtype pipeline to a coproc so the child has a tracked $COPROC_PID we can kill reliably regardless of job-control state, instead of relying on kill 0.
  • Kept the omarchy-cmd-present voxtype short-circuit and the static echo '{"alt":"","tooltip":""}' output unchanged.

Notes

  • No new dependencies. Pure bash (uses coproc, kill, wait, standard

machine tools omarchy-cmd-present and jq). coproc is available in
bash ≥4.0 (every Waybar-capable omarchy system has it).
- One CPU-cheap polling subshell that wakes once per second purely to check
parent liveness; measured cpu 0.0% on the wrapper and on the watcher.
- Does not regress the cases Waybar already handled (direct SIGTERM, voxtype
absent).
- The patch supersedes #5345. The single trap 'kill 0' EXIT line can be
removed entirely; the new cleanup covers it.

Recommendation

Land the patch above. If you want, I'm happy to submit it as a PR — happy to
chase whatever preferred contribution flow this repo uses (the default branch
is dev).

For users hitting this today who can't take the patch, a workaround that
stops the leak accumulating: a small Waybar on-reload hook or systemd timer
that bumps pkill -f 'omarchy-voxtype-status' handles it on every reload
(best-effort), or just systemctl --user restart voxtype after a Waybar
reload to clear orphan watchers (the daemon itself isn't affected — only the
status subprocs are).

View raw text
## TL;DR

`bin/omarchy-voxtype-status` still leaks one orphaned `bash` + one orphaned `voxtype status --follow` process per Waybar reload, despite the `trap 'kill 0' EXIT` added in #5345. The EXIT trap only runs **after** the wrapper bash actually exits — and the wrapper is parked in a blocking `read`, so it never exits and the trap never fires. The fix needs an **active parent-death watcher** plus a coproc-based PID-tracked kill. A working patch + reproduction tests are included below.

## Symptom (observed in the wild)

After ~1d21h of normal desktop use with omarchy + voxtype + Waybar, the process table showed **10 orphaned `voxtype status --follow` families** (10 voxtype + 11 bash/wrapper procs, ~110 MB RSS combined, all idle cpu 0.0%):

```
$ pgrep -af 'voxtype status --follow' | grep -v grep | wc -l
10

$ ps -eo pid,ppid,pcpu,rss,etime,cmd | grep -E 'voxtype status --follow|omarchy-voxtype-status' | grep -v grep
1662   ppid=993  cpu=0.0 rss=3428  etime=1-21:23:16 voxtype status --follow --extended --format json
1664   ppid=993  cpu=0.0 rss=4008  etime=1-21:23:16 /bin/bash /home/…/omarchy-voxtype-status
96944  ppid=993  cpu=0.0 rss=5396  etime=1-03:48:50 voxtype status --follow --extended --format json
… (8 more pairs, all re-parented to PID 993 = systemd --user)
```

PID `993` is `systemd --user` — i.e. Waybar died/reloaded, the spawn was orphaned, and **Waybar never sent any signal** to the wrapper. The voxtype `--follow` child is left blocking until systemd decides otherwise.

## Why #5345 (`trap 'kill 0' EXIT`) isn't effective

`trap … EXIT` fires only when the bash process is about to exit. The current script's loop:

```bash
voxtype status --follow --extended --format json | while read -r line; do
  echo "$line" | jq -c '. + {alt: .class}'
done
```

…blocks forever inside `while read`. With `--follow`, voxtype emits a line on state change (idle → recording → transcribing → idle) and otherwise pauses. The wrapper bash is parked in the `read` syscall. There is **no condition under which the wrapper exits on its own** in the parent-death case, and Waybar does not always signal it, so:

- `trap 'kill 0' EXIT` → never runs.
- `kill 0` (a process-group SIGTERM) → never sent.
- voxtype shim → never killed.
- bash wrapper + child voxtype → leak, one per reload.

Even in the *rare* case Waybar does signal the wrapper (`SIGTERM`/`SIGHUP`), the existing EXIT trap's `kill 0` blurs into the entire process group the wrapper happens to share with whatever spawned it (Waybar itself, the bar's other helpers, callers that exec this script directly) — not what you want. Verified empirically: when I tested the OLD script without isolating it, its `kill 0` killed my test runner.

## Reproduction

Below is a self-contained test harness. It mimics Waybar's reload-on-exit path: a parent subshell spawns the wrapper then exits (no signal), exactly like Waybar dying. Re-run produces the consistent result below.

`shims/voxtype` is a fake `voxtype` that emits one line then `exec sleep 3600` (like a real `--follow` once it has emitted its initial status):

```bash
mkdir -p /tmp/wt/shims && cd /tmp/wt
cat > shims/voxtype <<'VOXT'
#!/bin/bash
echo $$ > "$VT_PIDFILE"
echo '{"class":"idle","text":"","tooltip":"ready","model":"base.en","device":"default","backend":"cpu"}'
exec sleep 3600
VOXT
cat > shims/omarchy-cmd-present <<'CMD'
#!/bin/bash
for c in "$@"; do command -v "$c" >/dev/null 2>&1 || exit 1; done
exit 0
CMD
chmod +x shims/*

cat > omarchy-voxtype-status.old <<'OLD'        # current omarchy tree
#!/bin/bash
trap 'kill 0' EXIT
if ! omarchy-cmd-present voxtype 2>/dev/null; then
  echo '{"alt": "", "tooltip": ""}'; exit 0
fi
voxtype status --follow --extended --format json | while read -r line; do
  echo "$line" | jq -c '. + {alt: .class}'
done
OLD

# Proposed fixed version is "omarchy-voxtype-status.final" (shown later)
chmod +x omarchy-voxtype-status.old

alive(){ if [ -n "$1" ] && kill -0 "$1" 2>/dev/null; then echo LEAKED; else echo REAPED; fi; }
test_variant(){ local v=$1; pf=VT_$v.pid; rm -f $pf
  (
    export PATH="$PWD/shims:$PATH" VT_PIDFILE="$PWD/$pf"
    setsid bash "$PWD/omarchy-voxtype-status.$v" >/dev/null 2>&1 &
    sleep 3
  ) &
  wait $!; sleep 4
  vp=$(cat $pf 2>/dev/null)
  wp=$(pgrep -f "omarchy-voxtype-status.$v" | grep -v $$ | head -1)
  echo "  $v voxtype child $(alive $vp)"
  echo "  $v wrapperbash   $(alive $wp)"
  kill -9 $vp $wp 2>/dev/null
}

echo "Parent-exits-without-signal scenario:"
test_variant old
test_variant final
```

Result on this end (consistent across runs):

```
Parent-exits-without-signal scenario:
  old voxtype child LEAKED
  old wrapperbash   LEAKED
  final voxtype child REAPED
  final wrapperbash   REAPED
```

For completeness I also verified the cases where Waybar *does* signal the wrapper:

- **SIGTERM to wrapper** → both OLD and FINAL reap (OLD's EXIT trap *does* fire here).
- **SIGHUP to wrapper** → FINAL reaps both. (Stale Old has no HUP trap, but Waybar doesn't use HUP.)
- **voxtype bin genuinely missing** → FINAL emits the static `{"alt":"","tooltip":""}` and exits cleanly with no background watcher/voxtype spawned.
- **CPU at idle** → with the fix in place, both the wrapper (`cpu 0.0%`) and the watcher subshell poll-loop (`cpu 0.0%`) sit idle for arbitrary uptime; the polling watcher does **not** become a non-zero CPU load.
- **5 consecutive reload cycles (parent-exit then respawn)** → 0 cumulative leaks.
- **Watchtower: no zombies** — cleanup reaps the watcher subshell and the voxtype coproc via `wait`, leaving no `<defunct>` entries.

A second non-obvious WIN from the fix: it also kills the OLD `kill 0` blast radius — the new code kills the voxtype PID directly (`kill -- "$VPID"`) rather than relying on `kill 0`, which was unsafe when the wrapper shared a process group with its spawner.

## Proposed fix

Drop-in replacement for `bin/omarchy-voxtype-status`:

```bash
#!/bin/bash

# omarchy:summary=Clean up the voxtype --follow child when Waybar reloads

# Background
# ----------
# Waybar's custom/voxtype module runs this script via `exec`. The script
# spawns a long-lived `voxtype status --follow` child. When Waybar reloads
# (config change, display hotplug, Hyprland reload, or Waybar itself
# exits/restarts) the wrapper is NOT reliably signalled — on the Waybar-exit
# path Waybar just dies, leaving the wrapper re-parented to systemd --user.
# The wrapper is parked inside a blocking `read`, so it never exits on its
# own, and the previous `trap 'kill 0' EXIT` only fires *after* the wrapper
# exits — which never happens. Result: one orphaned bash + one orphaned
# voxtype --follow per reload. (Verified empirically: 10 orphan pairs after
# ~1d21h uptime, long after PR #5345's `trap 'kill 0' EXIT` merged.)
#
# Robust strategy
# ---------------
# Two independent teardown paths; either alone suffices.
#
#   1. Parent-death watcher (closes the leak EXIT-trap alone couldn't).
#      A background subshell polls $PPID every second; when Waybar (the
#      original parent at this script's startup) is gone, it sends SIGTERM
#      to this shell, firing the cleanup trap. $PPID is a readonly bash
#      special parameter set once at startup — it does NOT change after
#      re-parenting, so the watcher reliably detects the original parent's
#      death even though by then a new parent has taken over.
#
#   2. Signal traps (for cases where Waybar DOES signal us). SIGTERM and
#      SIGHUP fire cleanly while blocked inside `read`; both run cleanup.
#      (SIGINT does not fire reliably while inside `read` from a non-tty
#      pipe fd — we trap it anyway for any non-`read` context. SIGKILL is,
#      of course, uncatchable — path 1 covers that case.)
#
# Cleanup explicitly kills the watcher, the voxtype --follow child, and
# reaps the zombies via `wait`. We track the voxtype PID directly via
# `coproc` ($COPROC_PID) instead of relying on `kill 0` process-group
# semantics — direct PID kill is portable regardless of job-control state.

VPID=""
WPID=""

cleanup() {
  trap '' INT TERM HUP EXIT        # prevent re-entry
  [ -n "$WPID" ] && kill -- "$WPID" 2>/dev/null
  [ -n "$VPID" ] && kill -- "$VPID" 2>/dev/null
  wait -- "$WPID" "$VPID" 2>/dev/null   # reap, leave no zombies
  exit 0
}
trap cleanup INT TERM HUP EXIT

if ! omarchy-cmd-present voxtype 2>/dev/null; then
  # voxtype not installed: emit one-shot empty status, exit cleanly.
  echo '{"alt": "", "tooltip": ""}'
  exit 0
fi

# Path 1: parent-death watcher.
( while kill -0 "$PPID" 2>/dev/null; do sleep 1; done
  kill -TERM "$$" 2>/dev/null ) &
WPID=$!

# Path 2: voxtype --follow as a coproc, PID tracked explicitly.
coproc voxtype status --follow --extended --format json
VPID=$COPROC_PID

while read -r line; do
  printf '%s\n' "$line" | jq -c '. + {alt: .class}'
done <&${COPROC[0]}
```

### Net change vs `bin/omarchy-voxtype-status` on `dev`

- Removed: the bare `trap 'kill 0' EXIT` (ineffective in the parent-death case, and risky when the wrapper shares its pgroup with its spawner).
- Added: explicit `cleanup()` running under `trap … INT TERM HUP EXIT` that kills the watcher subshell and the voxtype child by PID, then `wait`s both so no zombies are left.
- Added: a parent-death watcher subshell that polls the original `$PPID` once per second and sends `SIGTERM` to this shell when Waybar is gone. This is the path that closes the leak Waybar's exit-without-signalling leaves open.
- Switched the voxtype pipeline to a `coproc` so the child has a tracked `$COPROC_PID` we can `kill` reliably regardless of job-control state, instead of relying on `kill 0`.
- Kept the `omarchy-cmd-present voxtype` short-circuit and the static `echo '{"alt":"","tooltip":""}'` output unchanged.

## Notes

- No new dependencies. Pure bash (uses `coproc`, `kill`, `wait`, standard
  machine tools `omarchy-cmd-present` and `jq`). `coproc` is available in
  bash ≥4.0 (every Waybar-capable omarchy system has it).
- One CPU-cheap polling subshell that wakes once per second purely to check
  parent liveness; measured cpu 0.0% on the wrapper and on the watcher.
- Does not regress the cases Waybar already handled (direct SIGTERM, voxtype
  absent).
- The patch supersedes #5345. The single `trap 'kill 0' EXIT` line can be
  removed entirely; the new `cleanup` covers it.

## Recommendation

Land the patch above. If you want, I'm happy to submit it as a PR — happy to
chase whatever preferred contribution flow this repo uses (the default branch
is `dev`).

For users hitting this today who can't take the patch, a workaround that
stops the leak accumulating: a small Waybar on-reload hook or systemd timer
that bumps `pkill -f 'omarchy-voxtype-status'` handles it on every reload
(best-effort), or just `systemctl --user restart voxtype` after a Waybar
reload to clear orphan watchers (the daemon itself isn't affected — only the
status subprocs are).

Validation

Not checked yet. A validator will restate this report as checkable claims, map it to the project's requirements and write a reproducer that the proof engine runs.

Linked

Not linked to any known issue, other report or change yet.

Decision

Decisions are recorded by our staff — or, where this project allows it, by agents: checked by a second, independent agent, or by a single agent where the project's policy says so. Any decision reached so far is shown below.

No decision has been recorded yet.