Gamification of coding - execute any command with automatic logging and ability to auto-report issues on GitHub.
This repository contains two implementations that share the same behaviour and documentation:
- JavaScript / Bun — published to npm; release tags
js-v<version>, titles[JavaScript] <version>. - Rust — published to crates.io; release tags
rust-v<version>, titles[Rust] <version>.
Install using Bun:
bun install -g start-commandOr install the Rust version:
cargo install start-commandThe $ command acts as a wrapper for any shell command:
$ echo "Hello World"
$ ls -la
$ cat file.txt
$ bun test
$ git statusQuoted arguments keep their boundaries, so $ node -e "console.log('hi')" and
$ git commit -m "msg with spaces" reach the command exactly as typed. A single
quoted argument is still run as a shell script ($ 'cat file.txt | grep x').
See docs/USAGE.md for details.
When piping data to a command wrapped with $, put $ on the receiving command:
# Preferred - pipe TO the $-wrapped command
echo "hi" | $ agent
# Alternative - quote the entire pipeline (more verbose)
$ 'echo "hi" | agent'Both approaches work, but piping TO $ is simpler and requires fewer quotes.
# More examples
cat file.txt | $ processor
git diff | $ reviewer
echo "analyze this" | $ agent --verboseSee docs/PIPES.md for detailed guidance on piping, docs/USAGE.md for general usage, and docs/EXAMPLES.md for examples checked against the JavaScript and Rust CLIs.
You can also use natural language to execute common commands. The $ command supports pattern-based substitutions defined in substitutions.lino:
# Install NPM packages
$ install lodash npm package # -> npm install lodash
$ install 4.17.21 version of lodash npm package # -> npm install lodash@4.17.21
$ install lodash npm package globally # -> npm install -g lodash
# Clone repositories
$ clone https://github.com/user/repo repository # -> git clone https://github.com/user/repo
# Git operations
$ checkout main branch # -> git checkout main
$ create feature-x branch # -> git checkout -b feature-x
# Common operations
$ list files # -> ls -la
$ show current directory # -> pwd
$ create my-project directory # -> mkdir -p my-project
# Python packages
$ install requests python package # -> pip install requestsIf no pattern matches, the command is executed as-is.
Commands can be expressed in plain English using patterns defined in substitutions.lino. This file uses Links Notation style patterns with variables.
Each pattern is defined as a doublet link - a pair of pattern and replacement wrapped in parentheses:
# Pattern definition in substitutions.lino:
(
install $packageName npm package
npm install $packageName
)
# Usage:
$ install express npm package
# Executes: npm install express
Variables like $packageName, $version, $repository are captured and used in the substitution.
All command output is automatically saved to your system's temporary directory. Output uses a "timeline" format with clear visual distinction:
│ session abc-123-def-456-ghi
│ start 2024-01-15 10:30:45
│
$ bun test
... command output ...
✓
│ finish 2024-01-15 10:30:52
│ duration 7.456s
│ exit 0
│
│ log /tmp/start-command/logs/direct/abc-123-def-456-ghi.log
│ session abc-123-def-456-ghi
The │ prefix indicates tool metadata, $ shows the executed command, and ✓/✗ indicates success/failure.
Every command gets an execution record that can be queried later:
# Show one execution by UUID or isolation session name
$ --status abc-123-def-456-ghi
# List all stored executions, newest first
$ --list
# Machine-readable list output
$ --list --output-format json
# Only the executions that are still running
$ --list --running
# Upload the stored log for one execution
$ --upload-log 29d6c026-b168-44a6-8a3f-c3919c7e5327
# Ask a detached isolated execution to stop gracefully
$ --stop 29d6c026-b168-44a6-8a3f-c3919c7e5327
# Terminate a detached isolated execution immediately
$ --terminate 29d6c026-b168-44a6-8a3f-c3919c7e5327
# Re-enter a running detached session
$ --attach my-docker-session
# Follow its output without sending input
$ --attach my-docker-session --read-only
# Restart the stored command in the same environment
$ --resume my-docker-session
# Run a different command in the same container filesystem
$ --resume my-docker-session -- bash
# Re-attach or reconcile every execution still marked running
$ --resume-all--status and --list default to Links Notation. Both also support
--output-format json and --output-format text. Status and list output
include best-effort processIds for tracked wrapper processes and detached
screen, tmux, and Docker isolation containers when those native tools can
report them.
For detached Docker executions, oomKilled is reported as an observation of the
container cgroup flag, not as a verdict: while docker inspect still reports the
container as running the status stays executing, and once it stops the reported
exitCode is the container's real exit code. 137 is only used as a fallback
when the container is gone and neither a stored exit code nor a log footer can be
recovered.
When a detached Docker container stops, the completion watcher writes the
terminal state back into the store, so --status reports a finished execution
instead of one that stays executing forever. The watcher waits until docker
reports the container as no longer running — log capture can stop early when
the log's disk fills up or dockerd restarts — and it never removes or finalizes
a container that is still running.
status answers a single question — is this execution still running? A
executed record only means the execution is over; it never means the command
succeeded. The cause of death lives in the other fields: exitCode (decoded as
137 (SIGKILL - 128+9) for a signal), oomKilled, exitReason, and the
post-mortem block written into the log.
endTime always says where it came from, in endTimeSource:
endTimeSource |
Meaning |
|---|---|
docker-finished-at |
docker inspect reported State.FinishedAt — a real clock reading. |
log-footer |
Read from the Finished: line the run wrote into its own log. |
observed-at |
Nothing recorded a finish time; this is when start noticed, and the same value is repeated in observedAt. |
A record whose execution was lost rather than observed ending — a host reboot,
a killed supervisor — keeps endTime empty and carries staleDetectedAt
instead, because the moment cleanup ran is not the moment the command stopped.
Whenever a Docker container is kept for investigation, the log ends with the
facts docker inspect still had while the container existed:
=== Container post-mortem ===
Container: my-docker-session
Exit Code: 137 (SIGKILL - 128+9)
OOMKilled: false
StartedAt: 2026-09-15T22:21:40.942007645Z
FinishedAt: 2026-09-15T22:21:46.740817278Z
Lifetime: 5.798s
Error: (none)
A removed container states the same facts in one line, before it stops existing:
Container removed: my-docker-session (exit 137, SIGKILL, lifetime 5.798s, oomKilled=false)
On a cgroup v2 host the watcher also samples the container's own memory
counters while it runs (issue #182) — memory.max, memory.peak and the
oom/oom_kill counters of memory.events — once a second, and once more
after the container exits, so the numbers outlive the cgroup. They follow the
post-mortem (or the one-line removal note) as one more line:
Memory: memory.max=268435456 memory.peak=268300000 oom=0 oom_kill=3 (OOM kill scope unknown)
State.OOMKilled is one sticky, container-wide boolean; these counters record
processes killed by any OOM killer (oom_kill), allocation events reaching
the memory limit (oom), and peak usage (memory.peak of memory.max).
The event counters are hierarchical and have different units: one group OOM
can kill several processes, and earlier allocation failures can coexist with a
later host OOM. Comparing these counts cannot distinguish container, parent or
host scope, so observed kills are labeled OOM kill scope unknown (issue #185).
Scope requires separately attributed kernel/cgroup evidence; a missing oom
count stays unknown. See the kernel memory interface documentation.
--status stores the raw counters as cgroupMemory
(limitBytes, peakBytes, oomEvents, oomKills) and shows them as
Cgroup Memory: peak 255.9 MiB of 256.0 MiB limit, oom 0, oom_kill 3 (...).
A non-zero oomKills also explains a SIGKILL or unknown exit as
memory-exhaustion (cgroup-oom-killer) (cgroup memory.events reported oom_kill=3) when State.OOMKilled was not set, and
each --on-kill-resume entry in recoveryHistory gets oomEvents=N, oomKills=M.
Limits: only cgroup v2 hosts and detached Docker executions are sampled; the
watcher reads the host's /proc and /sys/fs/cgroup, so a remote
DOCKER_HOST (or a cgroup v1 host) simply records nothing; a one-second
interval can miss kills in the last second when the cgroup disappears before
the final read; memory.peak needs Linux 5.19 or newer (otherwise peak unknown).
--upload-log accepts either an execution UUID or an isolation session name. It
looks up the stored logPath, installs gh-upload-log with Bun or npm if the
uploader is missing, and then streams the uploader output directly.
--stop and --terminate accept either the execution UUID or the isolation
session/container name. --stop asks the backend to stop gracefully (CTRL+C for
screen/tmux, docker stop for Docker). --terminate uses the backend's
immediate termination command.
--attach, --resume and --resume-all accept the same identifiers as
--status: an execution UUID or an isolation session name.
--attach <id> re-enters a running detached session — docker attach,
screen -r, or tmux attach-session. Add --read-only to follow the output
without sending input (docker logs -f, tmux attach-session -r, or a log
tail). If the session is already gone, --attach says so and points at
--resume instead of leaving you with a docker exec command that cannot work
on a stopped container.
--resume <id> continues a stopped detached execution:
| Session state | What happens |
|---|---|
| Container exists, no new command | docker start re-runs the stored command in the same container. |
| Container exists, new command given | The container filesystem is committed to an image and a derived container runs the new command. |
| Session is gone | The command is launched again through the stored isolation options (same image, volumes, env, networks). |
--resume <id> -- <command> is the form downstream tools need: it runs a
different command against the same container filesystem, instead of
docker start -ai, which would re-run the original entrypoint from scratch.
docker commit does not capture a container's HostConfig, so before committing
--resume reads the stopped container's resource limits with docker inspect
(including limits a supervisor applied later with docker update) and re-applies
the non-default ones to the derived <name>-resume-N container: --memory,
--memory-swap, --memory-reservation, --cpus (or --cpu-quota/--cpu-period),
--cpu-shares, --cpuset-cpus, --cpuset-mems, --pids-limit, --shm-size,
--storage-opt and --ulimit. The re-applied flags are printed as an
[Isolation] Resource limits: ... line and stored as resourceLimits in the
execution record, so --status shows them.
--on-kill-resume <N> lets a detached Docker execution recover from an OOM kill
(or any SIGKILL, exit code 137) without losing its container state. When the
main process is killed, the completion watcher restarts the same container
with docker start and runs --recovery-command (or, without it, the original
command) inside it — up to N times. The execution keeps its UUID and its log
file; each attempt is separated in the log by a [Recovery k/N] line, and the
command can read START_COMMAND_RECOVERY_ATTEMPT to know it is resuming.
$ --isolated docker --detached --on-kill-resume 2 \
--recovery-command 'agent --resume-from-checkpoint' -- agent --task build--on-kill-resume-delay <min[-max]> waits a uniformly random number of seconds
before each such resume (30-90, or one number for a fixed delay; the default
0 resumes at once). One host-wide OOM event often kills several executions at
the same moment; without a delay every one of them is restarted in the same
second, rebuilds its working set at once and triggers the next OOM event. The
chosen delay is printed in the [Recovery k/N] line (... resuming container box after a 63.2s delay, ...), and a --stop (or --terminate) during the
wait cancels the pending resume.
$ --isolated docker --detached --on-kill-resume 3 --on-kill-resume-delay 30-90 -- cargo test--status reports onKillResume, recoveryCommand, onKillResumeDelay,
recoveryAttempts and a recoveryHistory entry (exit code, OOM flag, the
killed run's cgroup oomEvents/oomKills when they were sampled, the delay as
delayMs when one is configured, start/finish time) per recovery. The killed
run's Memory: line is written into the log right before the [Recovery k/N]
separator, and cgroupMemory is cleared for the resumed run.
When all attempts are used, the execution is finalized with the last exit code.
Limits applied with docker update survive, because the container is reused.
Limitations: recovery needs execution tracking and a detached session whose only
isolation level is docker; --stop cancels any further recovery, but a
docker stop/docker kill issued outside $ looks like a kill and is recovered;
nothing is recovered if the container has been removed; and a snapshot resume
(--resume <id> -- <command>) or a relaunch of a removed session does not carry
the recovery options forward. Because the recovery marker stays in the
container, a later plain --resume <id> (docker start) runs the recovery
command rather than the original command.
Only a killed main process is recovered. Docker sets OOMKilled when any
process in the container was OOM-killed (a compiler, a test runner, a child
node) and keeps it set until the container is started again, so a main
process that survived that and then exited 0–127 on its own is not resumed:
its exit code stands, and oomKilled: true is still reported by --status and
the post-mortem as an observation. OOMKilled counts as a kill only when there
is no usable exit code (issue #178).
A resume keeps the original execution UUID, so --status, --list and
--upload-log keep addressing one logical session across restarts. The previous
session name is remembered in sessionNameHistory and still resolves to the
same record.
--resume-all repairs state after the supervisor host restarts, which kills the
detached completion watcher while the container keeps running. Each execution
still marked running is reported with one of four actions:
| Action | Meaning |
|---|---|
reattached |
A live Docker container got a fresh completion watcher. |
running |
A live screen/tmux session needs nothing; its logging is in-session. |
reconciled |
The session is gone, so the record was finalized from its exit code/footer. |
unknown |
The backend cannot be probed locally (ssh); the record is left untouched. |
--resume-all never silently restarts work: continuing a command is always an
explicit, per-session decision made with --resume. Use --list --running as
the machine-readable set that drives it.
A bare exitCode 139 with oomKilled false hides the real cause. When the
stored log contains a fatal memory marker such as
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory,
--status adds a hint:
Exit Reason: memory-exhaustion (v8-heap-limit)
Memory Exhausted: true
Memory Evidence: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
memoryExhausted answers the narrower question consumers of oomKilled are
really asking - did this run die of memory exhaustion? - and
memoryExhaustedReason carries the log line that proves it. A runtime that
aborts on its own heap limit dies below the container limit, so the kernel
never OOM-kills anything and State.OOMKilled stays false; the only evidence
is what the dying runtime printed into the log. Both fields appear only for a
non-zero exit code, so a command that merely prints such a marker and then
succeeds is never reported as a memory failure.
The same tail is scanned for attached and detached sessions alike, with a 64 KiB window, because V8 prints a long native stack trace after the marker.
Without a log marker, Docker's State.OOMKilled explains the exit
(memory-exhaustion (cgroup-oom-killer), Docker reported State.OOMKilled=true)
only when the command itself was SIGKILLed (exit 137) or its exit code is
unknown. The flag is container-wide and sticky (moby/moby#43564): it turns on
when any process in the container is OOM-killed, so a cargo test whose
rustc child was OOM-killed and which later exited 1 on its own reports
oomKilled true with no memory exit reason (issue #180).
exitReason, memoryExhausted and memoryExhaustedReason are only hints. They
never change status, exitCode or oomKilled, which stay observations of what
the backend actually reported.
The exit code is always prominently displayed after command completion, making it clear whether the command succeeded or failed.
When a command fails (non-zero exit code) and it's a globally installed NPM package:
- Repository Detection - Automatically detects the GitHub repository for NPM packages
- Log Upload - Uploads the full log to GitHub (requires gh-upload-log)
- Issue Creation - Creates an issue in the package's repository with:
- Command that was executed
- Exit code
- System information
- Link to uploaded log
│ session abc-123-def-456-ghi
│ start 2024-01-15 10:30:45
│
$ some-npm-tool --broken-arg
... error output ...
✗
│ finish 2024-01-15 10:30:46
│ duration 1.789s
│ exit 1
│
│ log /tmp/start-command/logs/direct/abc-123-def-456-ghi.log
│ session abc-123-def-456-ghi
Detected repository: https://github.com/owner/some-npm-tool
Log uploaded: https://gist.github.com/user/abc123
Issue created: https://github.com/owner/some-npm-tool/issues/42
Run commands in isolated environments using terminal multiplexers, containers, or remote servers:
# Run in tmux (attached by default)
$ --isolated tmux -- bun start
# Run in screen detached
$ --isolated screen --detached -- bun start
# Run in docker container
$ --isolated docker -- echo "hello from docker"
# Run a Bun command in the link-foundation/box JavaScript image
$ --isolated docker --image ghcr.io/link-foundation/box-js:latest -- bun --version
# Run a multi-runtime AI coding experiment in the full box image
$ --isolated docker --image ghcr.io/link-foundation/box:latest -- bash -lc 'node --version && python --version && rustc --version'
# Mount tool credentials and pass environment variables into the container
$ -i docker --image konard/hive-mind-dind:latest \
-v ~/.config/gh:/root/.config/gh \
-v ~/.claude:/root/.claude \
-e GH_TOKEN=$GH_TOKEN -- gh repo list
# Run a Docker-in-Docker image in privileged mode
$ -i docker --image konard/hive-mind-dind:latest --privileged -- solve <issue-url>
# Run on remote server via SSH
$ --isolated ssh --endpoint user@remote.server -- npm test
# Short form with custom session name
$ -i tmux -s my-session -d bun startCreate a new isolated user with the same group permissions as your current user to run commands in complete isolation:
# Create an isolated user with same permissions and run command
$ --isolated-user -- npm test
# Specify custom username for the isolated user
$ --isolated-user myrunner -- npm start
$ -u myrunner -- npm start
# Combine with process isolation (screen or tmux)
$ --isolated screen --isolated-user -- npm test
# Keep the user after command completes (don't delete)
$ --isolated-user --keep-user -- npm start
# The isolated user inherits your group memberships:
# - sudo group (if you have it)
# - docker group (if you have it)
# - wheel, admin, and other privileged groupsThe --isolated-user option:
- Creates a new system user with the same group memberships as your current user
- Runs the command as that user
- Automatically deletes the user after the command completes (unless
--keep-useris specified) - Requires sudo access without password (NOPASSWD configuration)
- Works with screen and tmux isolation environments (not docker)
This is useful for:
- Running untrusted code in isolation
- Testing with a clean user environment
- Ensuring commands don't affect your user's files
| Environment | Description | Installation |
|---|---|---|
screen |
GNU Screen terminal multiplexer | apt install screen / brew install screen |
tmux |
Modern terminal multiplexer | apt install tmux / brew install tmux |
docker |
Container isolation (uses a default image, or --image) |
Docker Installation |
ssh |
Remote execution via SSH (requires --endpoint) | apt install openssh-client / brew install openssh |
| Option | Description |
|---|---|
--isolated, --isolation, -i |
Isolation environment (screen, tmux, docker, ssh) |
--attached, -a |
Run in attached/foreground mode (default) |
--detached, -d |
Run in detached/background mode |
--session, -s |
Custom session/container name |
--image |
Docker image (optional; defaults to OS-matched image) |
--volume, -v |
Docker bind mount/volume host:container[:mode] (repeatable, docker only) |
--mount |
Docker --mount spec (repeatable, docker only) |
--env, -e |
Environment variable KEY=VALUE for the container (repeatable, docker only) |
--privileged |
Run docker container in privileged mode (docker only) |
--network |
Connect to a named network (repeatable, docker only) |
--network-alias |
Add an alias on the first network (repeatable, docker only) |
--endpoint |
SSH endpoint (required for ssh, e.g., user@host) |
--isolated-user, -u [name] |
Create isolated user with same permissions (screen/tmux) |
--keep-user |
Keep isolated user after command completes (don't delete) |
--keep-alive, -k |
Keep session alive after command completes |
--auto-remove-docker-container |
Always remove docker container after exit (docker only) |
--always-cleanup-container |
Always remove docker container after exit (docker only) |
--keep-container |
Keep docker container filesystem after exit (docker only) |
--keep-container-on-fail |
Keep failed or OOM-killed docker containers after exit (docker only) |
--on-kill-resume <N> |
Resume up to N times after an OOM kill/exit 137 (detached docker only) |
--recovery-command <cmd> |
Command to run in the same container on such a resume (implies one attempt) |
--on-kill-resume-delay <s> |
Wait a random <min>[-<max>] seconds before each such resume (default 0) |
Note: Using both --attached and --detached together will result in an error - you must choose one mode.
When --network is repeated, Docker creates the container on the first network,
connects every additional network, and only then starts the command. This lets a
container retain egress through one network while reaching services on a private
network without a startup race. Repeated --network-alias values apply to the
first network.
$ --isolated docker --image alpine:3.23 \
--network bridge --network my-sidecar-net -- ping -c 1 sidecarBy default, all isolation environments (screen, tmux, docker) automatically exit after the target command completes. This ensures resources are freed immediately and provides uniform behavior across all backends.
Use --keep-alive (-k) to keep the session running after command completion:
# Default: session exits after command completes
$ -i screen -d -- echo "hello"
# Session will exit automatically after command completes.
# With --keep-alive: session stays running for interaction
$ -i screen -d -k -- echo "hello"
# Session will stay alive after command completes.
# You can reattach with: screen -r <session-name>For Docker containers, successful runs are removed by default. Failed containers, including containers Docker reports as OOMKilled, are kept for investigation and include a docker rm -f <container> cleanup hint. Use --always-cleanup-container or --auto-remove-docker-container to force removal after exit, or --keep-container to preserve the container filesystem after every run.
The tool works in any environment:
- No
ghCLI? - Logs are still saved locally, auto-reporting is skipped - No
gh-upload-logduring auto-reporting? - Issue can still be created with local log reference - No
gh-upload-logduring manual--upload-log? - The uploader is installed on demand - Repository not detected? - Command runs normally with logging
- No permission to create issue? - Skipped with a clear message
- Isolation environment not installed? - Clear error message with installation instructions
- Bun >= 1.0.0
- GitHub CLI (
gh) - For authentication and issue creation - gh-upload-log - For uploading log files
To set up auto-reporting:
# Install GitHub CLI and authenticate
gh auth login
# Install log uploader
bun install -g gh-upload-log- Command Execution - Your command is passed directly to the shell (bash/powershell/sh)
- Output Capture - Both stdout and stderr are captured while still being displayed
- Log File - Complete output is saved with timestamps and system info
- Failure Handling - On non-zero exit:
- Detects if the command is an NPM package
- Looks up the package's GitHub repository
- Uploads log (if
gh-upload-logis available) - Creates an issue (if
ghis authenticated and has permission)
The following environment variables can be used to customize behavior:
| Variable | Description |
|---|---|
START_DISABLE_AUTO_ISSUE |
Set to 1 or true to disable automatic issue creation |
START_DISABLE_LOG_UPLOAD |
Set to 1 or true to disable log upload |
START_LOG_DIR |
Custom directory for log files (defaults to OS temp directory) |
START_VERBOSE |
Set to 1 or true for verbose output |
START_DISABLE_SUBSTITUTIONS |
Set to 1 or true to disable pattern matching/aliases |
START_SUBSTITUTIONS_PATH |
Custom path to substitutions.lino file |
Example:
# Run without auto-issue creation
START_DISABLE_AUTO_ISSUE=1 $ bun test
# Use custom log directory
START_LOG_DIR=./logs $ bun test
# Disable substitutions (use raw command)
START_DISABLE_SUBSTITUTIONS=1 $ install lodash npm package
# Use custom substitutions file
START_SUBSTITUTIONS_PATH=/path/to/my-rules.lino $ install mypackage npm packageYou can create your own substitution patterns by placing a substitutions.lino file in ~/.start-command/substitutions.lino. User patterns take precedence over the default ones.
Log files are saved under /tmp/start-command/logs/ by default and contain the command output along with metadata. When an execution UUID is available, the log path is stable, for example /tmp/start-command/logs/direct/<uuid>.log or /tmp/start-command/logs/isolation/screen/<uuid>.log. The console output uses a "timeline" format:
│ session abc-123-def-456-ghi
│ start 2024-01-15 10:30:45
│
$ bun test
... command output ...
✓
│ finish 2024-01-15 10:30:52
│ duration 7.456s
│ exit 0
│
│ log /tmp/start-command/logs/direct/abc-123-def-456-ghi.log
│ session abc-123-def-456-ghi
The log file itself contains the raw command output and execution metadata.
Unlicense (public domain)
This project is released into the public domain under the Unlicense. It has fewer restrictions and more freedoms than MIT — especially for commercial use. You can copy, modify, publish, use, compile, sell, or distribute this software without any conditions or attribution requirements.