A tiny, daemonless process launcher and recorder.
sigmund is a lightweight launcher that protects detached processes from CI/runner process-group cleanup and gives you a durable, safe handle to manage them later.
Many CI runners (like GitHub Actions or GitLab CI) and non-interactive job systems terminate the invoking shell’s process group at step completion. If you start a long-running process (like QEMU, a test database, or a web server) in the background using standard shell tools like nohup cmd &, it will be killed the moment the step finishes.
sigmund prevents this by launching the command in a new session / process group and recording a small state file. This allows you to safely background tasks, capture their logs, and tear down their entire process tree cleanly when you are done.
(Name note: in Old English and related Germanic languages, mund relates to “protection/guardianship,” so sigmund reads naturally as “signal protection.”)
- More complete than
setsid: While runningsetsid cmd &successfully escapes the CI runner's process group, it leaves you blind. You have to manually track PIDs, wire up log files, and you still suffer from the risk of PID recycling when you script the teardown.sigmundgives you the session isolation ofsetsidplus durable tracking and safe cleanup. - Better than
nohup &: Prevents orphan processes.sigmund stopkills the entire process group, ensuring child processes don't leak into the background forever. - Safer than bare
kill $PID: Immune to PID recycling.sigmundverifies/proc/<pid>/statstart times and/proc/<pid>/exeinodes before sending signals, so you never accidentally kill a critical system service days later. - Lighter than
systemd-runortmux: Zero dependencies, no background daemon, no D-Bus required. Just a single compiled C binary.
Build:
Requires a C11 compiler. Linux-first (relies on /proc for identity validation).
By default, make produces a static standalone binary (-static) named sigmund, so it does not depend on the host glibc version at runtime.
make
./sigmund --help
# Optional: build a dynamically linked binary instead (smaller, host-glibc dependent)
make sigmund-dynamic
# output: ./sigmund-dynamicFor cross-platform CI and releases, build and publish both variants:
- Static artifact (
make): best portability across Linux hosts. - Dynamic artifact (
make sigmund-dynamic): smaller binary when runtime glibc compatibility is acceptable.
Basic Usage:
# Start a detached process
$ sigmund qemu-system-x86_64 -m 4096 -nographic
sigmund: id=7f3c2a pid=4012 pgid=4012 sid=4012
sigmund: log: /run/user/1000/sigmund/7f3c2a.log
sigmund: stop: sigmund stop 7f3c2a
# List tracked runs
$ sigmund list
RUNID STATE STARTED_AT RESULT CMD
7f3c2a running 2026-04-09T18:42:11Z - qemu-system-x86_64 -m 4096...
# Stop the run cleanly (sends SIGTERM, waits, then SIGKILL if needed)
$ sigmund stop 7f3c2a(Note: Use sigmund -- <cmd> if your command name overlaps with a sigmund subcommand).
When running integration tests, you often need to spin up a server, run tests against it, and tear it down. sigmund handles the backgrounding, logging, and cleanup automatically.
# Example CI Step
- name: Start Test Database
run: |
# Starts in a new session, immune to this step's teardown
sigmund -- redis-server --port 6379
sleep 2 # wait for boot
- name: Run Test Suite
run: npm run test:integration
- name: Teardown
if: always()
run: |
# Find and stop the redis server
RUN_ID=$(sigmund list | grep redis-server | awk '{print $1}')
sigmund stop $RUN_ID
sigmund pruneIf you are testing a complex local architecture (e.g., a frontend watcher, a backend API, and a worker queue), you can use sigmund to spin them up into the background without keeping multiple terminal tabs open, and without losing track of them.
sigmund npm run dev:frontend
sigmund npm run dev:backend
sigmund celery -A myapp worker
# Later, when you want to stop working:
sigmund list
# Stop them individually, or script a teardown of all running jobs| Command | Description |
|---|---|
sigmund <cmd...> |
Starts a command in a new process group. |
sigmund --tail <cmd...> |
Starts a command and immediately follows its log. |
sigmund -- <cmd...> |
Starts a command whose name overlaps with a subcommand. |
| Command | Description |
|---|---|
sigmund list |
Lists all tracked runs with RUNID, STATE, STARTED_AT, RESULT, and CMD. |
sigmund tail <id> |
Follows the log for an already-running tracked process. |
sigmund dump <id> |
Prints the saved output log for a run and exits (works for stale runs if log exists). |
sigmund stop <id> |
Gracefully stops a run. Sends SIGTERM to the group, waits up to 5s, then sends SIGKILL. |
sigmund kill <id> |
Forcefully kills a run immediately using SIGKILL. |
sigmund killcmd <id> |
Prints the raw shell command needed to signal the process group (e.g., kill -TERM -- -4012). Refuses stale runs. |
sigmund prune |
Backward-compatible cleanup: removes exited/failed records and orphan logs. |
sigmund prune <id> |
Removes exactly one prunable run record and its associated log/output. |
sigmund prune all |
Removes all prunable runs (stale, exited, failed) and their associated output. |
| Switch | Description |
|---|---|
--tail |
Launches a command and immediately streams its log output. |
Note:
--is an argument separator, not a switch. Use it when your command name could be interpreted as asigmundcommand. Example:sigmund -- list.
sigmund always captures child process output:
stdinis always redirected from/dev/null.stdoutandstderrare always redirected to a dedicated per-run log file stored next to the state record.- Start output always includes the log path and a ready-to-run stop command.
Use sigmund --tail <cmd> [args...] to launch exactly the same way and then follow the log in your terminal.
Use sigmund tail <id> to follow the log of an already-running tracked process.
Press Ctrl-C to detach from tailing while the background process keeps running.
sigmund tracks state in ~/.local/state/sigmund. This is intentionally a persistent per-user location rather than $XDG_RUNTIME_DIR, because persistent state/logs are more predictable across shells, CI environments, WSL setups, and hosts where XDG runtime handling is inconsistent. Records and logs survive reboot; launched processes do not. All state updates use atomic file renames (rename() + fsync()) so records are never corrupted, even during power loss.
Strict Identity Validation:
Before sending any signal, sigmund checks:
- Does the system
boot_idmatch the one recorded? (Prevents signaling the wrong process after a reboot). - Does
/proc/<pid>/statstart-time match? (Prevents signaling if the PID rolled over and was reassigned). - Do the executable device/inode numbers in
/proc/<pid>/exematch? (Prevents signaling if a different binary took the PID).
If any check fails (including boot mismatch), the state evaluates as stale and signals are blocked (stop, kill, killcmd all refuse stale runs). Stale records remain visible in list until explicitly pruned.
sigmund does not restart processes after reboot and is not a supervisor.
Edge Cases Handled:
- If the original leader PID exits but child processes in the group remain alive,
sigmund stopstill targets the group safely. - Very fast commands may exit before
/proccan be read; this is treated as non-fatal and will accurately show asdeadon the next list. - Warns if child processes "escape" the process group (e.g., a child calls
setsid()itself) but remain in the session.
- MVP completeness: start/list/stop/kill/prune/killcmd
- Harden edge cases in CI runners:
- exec-handshake reliability
- leader-exits-but-group-lives behavior
- Optional Linux-only diagnostics:
- session-ID scan to warn on group escapes
Apache-2.0. See LICENSE.