Conditional command runner. Don't burn metered bandwidth on backups.
condrun gates command execution on live system-state predicates and kills the child process if those predicates flip mid-run. Wrap a restic/rsync/borg/duplicacy cron job with condrun and stop worrying about the 2am backup chewing through your phone tether.
Cron is dumb. It fires at the scheduled time regardless of whether you're on home Wi-Fi, on a Personal Hotspot, in Low Data Mode on the train, or off-network entirely. A bandwidth-sensitive job like restic backup doesn't care — it'll happily push 4GB through your cellular connection and surprise you with an overage bill.
condrun sits between cron and your job. Before launching the child, it samples the current network state. If the predicates fail (e.g. you asked for "not metered" and you're tethered), it exits silently — cron sees a clean run, no log spam. If the predicates pass, it spawns the child and keeps polling. The instant a predicate flips (you tether mid-backup, or toggle Low Data Mode), it sends SIGTERM, waits for a configurable grace period, then SIGKILLs.
Use cases: restic, rsync, borg, duplicacy, rclone, or any cron task where bandwidth class matters more than punctuality.
cargo install condrunOr build from a clone and install the binary directly:
git clone https://github.com/jordanbaker/condrun
cd condrun
make install # → ~/.local/bin/condrun (default)
make install PREFIX=/usr/local # → /usr/local/bin/condrun (needs sudo)
make uninstall # removePre-built binaries (when published) will be available from the GitHub releases page. A Homebrew tap (brew install jordanbaker/tap/condrun) is planned.
condrun currently runs on macOS only — predicate detection uses Apple's Network.framework (NWPathMonitor), which has no Linux/Windows equivalent. Linux support via NetworkManager / iwd is on the roadmap.
The motivating example. Drop this in your crontab:
0 2 * * * condrun run --reject-expensive --reject-low-data -- restic backup -r s3:bucket ~/workAt 2am every night:
condrunsamples the current network path.- If the connection is expensive (cellular, Personal Hotspot) or constrained (Low Data Mode on), it exits 0 silently. Cron is happy. No partial upload, no overage.
- Otherwise it spawns
restic backup, polls every 30 seconds, and kills it if you tether mid-job.
condrun run --reject-expensive -- rsync -av ~/photos backup-host:photos/Single predicate — only refuses to run on metered links, doesn't care about Low Data Mode.
| Flag | Behavior | macOS source |
|---|---|---|
--reject-expensive |
Pass iff current path is not marked expensive (cellular, Personal Hotspot) | NWPath.isExpensive |
--reject-low-data |
Pass iff Low Data Mode is off | NWPath.isConstrained |
SSID matching, AC-power gating, and interface-type predicates ship in v0.1.x. Track SPEC.md §4 for the predicate roadmap.
| Flag | Default | Meaning |
|---|---|---|
--strict |
false |
Exit 1 on pre-flight failure (default: silent exit 0) |
--kill-on-change |
true |
Kill the child if predicates flip after launch |
--no-kill-on-change |
— | Disable kill-on-change; child runs to completion regardless |
--grace 30s |
30s |
SIGTERM → SIGKILL grace period |
--poll 30s |
30s |
Watcher poll interval |
--debounce 0s |
0s |
Predicate must stay failed this long before triggering kill (debounces flapping) |
--any |
false |
Compose predicates with OR instead of AND |
Subcommands:
condrun run [predicates] -- <cmd> [args...]— gate, spawn, supervise.condrun check [predicates]— evaluate predicates and exit; useful for shell scripts and debugging.
Per SPEC.md §6:
| Code | Meaning |
|---|---|
0 |
Success, or pre-flight predicates failed in non-strict mode (silent skip) |
1 |
Pre-flight predicates failed under --strict |
2 |
Child exited non-zero (exit code preserved when possible) |
3 |
Child killed by condrun because predicates flipped mid-run |
4 |
CLI parse error, config error, or internal failure |
macOS exposes network-path metadata through NWPathMonitor in the Network framework. condrun instantiates a monitor, polls the current NWPath, and reads two boolean flags:
isExpensive— true on cellular interfaces and on Personal Hotspot. This is the OS's own classification of "billable bandwidth".isConstrained— true when the user has enabled Low Data Mode for the active interface (Wi-Fi or cellular).
Because these flags come from the OS, they correctly track Personal Hotspot even when the laptop sees the tether as plain Wi-Fi. There's no SSID heuristic to maintain, no per-carrier list, no guessing.
condrun is built around small trait-based seams so the supervisor logic can be tested without touching the OS:
NetworkState— samples the current path. Real impl wrapsNWPathMonitor; test impl is a static fixture.Predicate— pure function fromNetworkStatesnapshot to pass/fail.Spawner/ChildHandle— abstractstokio::process::Commandso the supervisor's race logic can be exercised against a fake child.Signals— abstracts SIGINT/SIGTERM delivery.- Supervisor —
tokio::select!race between poll-tick, child completion, and external signals. Owns the kill-on-change state machine (debounce, SIGTERM, grace, SIGKILL).
v0.1.0 is polling-only (--poll 30s). v0.2 will add event-driven NWPathMonitor updates so predicate flips are detected without waiting for the next tick.
# Unit tests against the production binary (no test scaffolding):
cargo test
# Unit + integration tests (requires the test-fixture feature for the
# fake spawner / fake network state used by the integration harness):
cargo test --features test-fixture
# Integration tests only, run serially (the CLI test harness mutates env):
cargo test --features test-fixture --test cli -- --test-threads=1
# Opt-in smoke tests against the real macOS Network.framework. Skipped in CI
# because GitHub Actions runners don't expose realistic network paths;
# meant to be run on a developer machine before tagging a release:
cargo test --features platform-testsLints and formatting:
cargo fmt --all -- --check
cargo clippy --all-features --all-targets -- -D warningsLicensed under either of Apache License 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.