Durable local process control for software agents.
Status: 0.2 release. The complete local lifecycle is implemented and tested on Linux, macOS, and Windows.
ProcHerd starts a command under a detached per-run supervisor and returns a stable run ID. A later shell, agent turn, or local tool can inspect state, wait for readiness, page through bounded logs, or stop the entire owned process tree.
procherd start \
--lease-port web \
--ready-port web \
-- npm run dev -- --port {port:web}
procherd wait run_01J... --for ready --timeout 60s
procherd logs run_01J... --after 0 --limit 50
procherd stop run_01J... --grace 5sShell backgrounding gives callers a PID, but not durable ownership. Agent workflows routinely lose background processes, fill pipes or context with output, race for local ports, confuse “spawned” with “ready,” and leave descendants behind.
ProcHerd makes those lifecycle boundaries explicit:
- stable
run_<ULID>identity independent of process IDs; - a detached supervisor per run, with no shared daemon to configure;
- Unix process groups or Windows Job Objects for tree cleanup;
- bounded stdout/stderr capture with monotonic cursors and full-stream digests;
- AND-composed TCP, HTTP, file, log-literal, and leased-port readiness;
- named loopback-port and private temporary-directory leases;
- maximum runtime, graceful shutdown, forced cleanup, and durable evidence;
- versioned JSON results, JSON Schemas, stable exit codes, and completions.
Download a native archive from GitHub Releases, or install from a source checkout with Rust 1.85 or newer:
cargo install --path . --lockedSee INSTALL.md for platform-specific, checksum- and provenance-verified native installation, updating, and removal.
Generate completion scripts with procherd completions bash (also zsh,
fish, powershell, and elvish).
Commands are always passed as an argument vector after --; ProcHerd never
reconstructs a shell string.
result=$(
procherd --format json start \
--max-runtime 10m \
--ready-log "listening" \
-- ./my-server --port 8080
)
run_id=$(printf '%s' "$result" | jq -r .run.run_id)
procherd --format json status "$run_id"
procherd --format json wait "$run_id" --for ready --timeout 30sWhen no readiness condition is supplied, start means the child was spawned;
it does not claim application readiness. wait --for exit returns only after
the terminal state is durable and the per-run supervisor has fully exited.
Named resources are delivered both through placeholders and environment variables:
procherd start \
--lease-port api \
--lease-temp-dir work \
--ready-port api \
-- ./server \
--port {port:api} \
--work-dir {temp:work}The child also receives PROCHERD_PORT_API and PROCHERD_TEMP_WORK. Names use
lowercase ASCII identifiers and are normalized to uppercase (with - changed
to _) in environment variables.
Temporary directories remain available for inspection until the run is
garbage-collected.
Port allocation is coordinated among ProcHerd runs and the listener is held
until immediately before spawn. It is intentionally reported as
coordinated_best_effort: operating systems do not provide an atomic transfer
of a listening socket to an arbitrary child that binds the port itself.
Logs are captured in separate stdout/stderr records using base64 so arbitrary bytes remain lossless:
procherd --format json logs run_01J... --after 0 --limit 200
procherd --format ndjson logs run_01J... --stream stderrUse next_after_cursor for the next page. Capture stops at
--max-log-bytes (16 MiB by default); the supervisor continues draining child
pipes through a fixed-capacity in-memory queue and reports dropped_bytes.
Live reads expose only records whose cursors are present in the same durable
state snapshot; poll again to observe records that were flushed while that
snapshot was being committed.
Final SHA-256 digests cover the complete stdout and stderr streams, including
bytes not retained as records. If output arrives faster than it can be hashed,
bounded pipe backpressure can temporarily throttle the child instead of
allowing supervisor memory to grow with the stream.
All commands accept --format human|json|ndjson. Machine-readable results use
versioned schema identifiers. Inspect the bounded contract or a full JSON
Schema without starting a process:
procherd --format json schema --document brief
procherd --format json schema --document run
procherd --format json schema --document start
procherd --format json schema --document logsStable exit-code classes are:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Operational failure |
| 2 | Invalid usage |
| 3 | Caller or readiness timeout |
| 4 | Run not found |
| 5 | Durable-state integrity failure |
See docs/CONTRACT.md for lifecycle and compatibility details.
The checked-in store compatibility corpus freezes a published v0.1 terminal run store byte-for-byte. CI reopens it through every read-only lifecycle view and rejects declared state and log corruptions on all supported operating systems.
Performance observations use a generated 1,000-run terminal store and a minimal start-to-running lifecycle. The benchmark methodology documents measurement boundaries, supervisor memory sampling, raw artifacts, and the distinction between the current baseline and future v1.0 regression thresholds.
The default state root is the platform user data directory:
- Linux:
$XDG_DATA_HOME/procherdor~/.local/share/procherd; - macOS:
~/Library/Application Support/procherd; - Windows:
%LOCALAPPDATA%\procherd.
Override it with --state-dir or PROCHERD_STATE_DIR. On Unix, new state
directories and files use owner-only permissions. gc is a dry run unless
--execute is supplied and rechecks that every target is terminal and
inactive immediately before deletion:
procherd --format json gc --older-than 7d
procherd --format json gc --older-than 7d --execute- ProcHerd executes the exact program requested with the caller's inherited environment. It is not a sandbox and does not restrict filesystem or network access.
- Captured logs and command arguments may contain secrets. Logs are bounded, but not redacted or encrypted.
- Status is readable by any principal that can read the state directory. Stop requests require the private per-run owner token stored there.
- Readiness is local-only: TCP and HTTP endpoints must resolve to loopback; HTTP 2xx and 3xx responses count as ready. HTTPS and credential-bearing URLs are rejected in 0.1.
- Commands that deliberately daemonize or escape their assigned process group may exceed Unix cleanup guarantees. Windows descendants remain in the Job Object unless they use operating-system escape privileges.
- A machine crash can leave a live-looking durable record. ProcHerd reports it
as
orphaned; automatic process reattachment is not a 0.1 guarantee.
Read SECURITY.md and docs/PLATFORM-SUPPORT.md before relying on ProcHerd for sensitive or long-lived workloads.
cargo fmt --check
cargo clippy --all-targets --locked -- -D warnings
cargo test --all-targets --locked
cargo package --locked --allow-dirtyThe E2E suite covers detached reacquisition, cursor pagination, log overflow, readiness success and false-positive prevention, lease handoff and collision avoidance, runtime limits, idempotent tree cleanup, supervisor failure, and safe garbage collection.
CI tests Linux, macOS, Windows, and Rust 1.85. Tagged releases contain native archives, documentation, completions, SHA-256 checksums, a CycloneDX SBOM, and GitHub/Sigstore build-provenance and SBOM attestations. See RELEASING.md.
Use GitHub Discussions for questions and workflow examples, and structured issues for reproducible bugs and scoped features. See CONTRIBUTING.md, SUPPORT.md, GOVERNANCE.md, and the Code of Conduct. Report vulnerabilities privately.
Verified, opt-in usage is recorded in ADOPTERS.md.
MIT