Run commands from a local workspace on a configured runner. For installation and the first run, start with the README.
Frequent execution flags follow familiar CLI conventions: -d is
--detach, -e is --env, -w is --workdir, and SSH-style -L is
--forward. errand kill -f requests --force, and GC accepts -n for
--dry-run; setup and peer mutation use the same -f and -n forms, while
peer discovery uses -a for --all. Safety boundaries, transport details,
and mutating workspace options remain long-form so their meaning stays explicit.
Use --help at the relevant level, for example errand peers --help or
errand gc jobs --help.
Errand returns the remote process's exit code. If that code is zero but the transaction fails, Errand exits 120. A secondary transaction failure is reported without replacing a nonzero process exit code. Detaching successfully returns zero for the detach action, before the remote command has finished.
If the runner stops answering while you follow a job, Errand does not wait out
the job's runtime limit. A runner writes a heartbeat on every open log stream
every few seconds, so a connection that goes quiet is noticed within 15
seconds and Errand reconnects, resuming the log where it stopped. When it
cannot get the stream back for 60 seconds it stops, says the runner is
unavailable and the job's state is unknown, prints the errand status and
errand attach commands for the job, and exits 120. It never reports that as
the job failing: the job may still be running, may have finished, or may have
been lost with the runner. If the runner still answers but cannot serve the
log, Errand asks it for the job's state and reports that instead. A Ctrl-C the runner never confirmed is called out
along with the errand kill command, so it is not mistaken for a stopped job.
Run errand from a Git worktree for automatic snapshot selection. A non-Git
directory requires an explicit .errandignore policy or --include-all.
Errand always refuses a filesystem root, and snapshotting your home directory
requires --include-all. The client prints the selected file count and byte
total before remote admission. Use --no-snapshot when a command needs no
local files; Errand then skips local content inspection and runs it in an empty
remote workspace. Errand still records the invocation directory's identity so
retained changes can be applied there safely. Because the remote workspace is
empty, --workdir may only name its root.
Before submission, Errand shows the full local workspace and command-directory
paths. Job status describes the workdir relative to the runner's workspace,
using workspace root for its root; local paths are not remote execution paths.
For a workspace containing several repositories, place this marker and an
explicit .errandignore at the shared root:
# .errand.toml
[workspace]
root = true
[run]
peer = "mac-mini" # optional; alias lives in personal config
[changes]
apply_on_success = trueErrand discovers the nearest marked ancestor, snapshots from there, and runs
the command in the caller's relative directory. --workspace-root PATH
selects the same boundary explicitly. Automatic discovery trusts only a marker
and directory owned by the caller in a directory that is not group- or
world-writable; use the explicit flag for intentionally shared workspaces. A
marker chooses only the boundary; it does not bypass .errandignore, Git
selection, home-directory protection, or the filesystem-root refusal.
[changes].apply_on_success is read only from the selected workspace root.
For --no-snapshot, the current directory is the configuration root; Errand
does not climb to an ancestor marker when no local workspace is transferred.
Set apply_on_success = true in ~/.config/errand/config.toml for a personal
default. Explicit --apply or --no-apply wins over workspace and personal
configuration. Applying is a property of the submitted job, independent of
whether its logs are currently being observed. --detach --apply and Ctrl-D
during an applying run hand completion to a detached local worker, which waits
for success and applies into the originating workspace. attach remains
observation-only and never chooses or changes that policy. If the worker or
client machine stops, other CLI commands do not restart it. Once the remote
job finishes, run errand fetch --apply HANDLE from its originating workspace
to recover and apply its retained changes. Healthy workers continue normally,
including their existing retries for temporary connection failures.
If a completed job produced no changes, fetch --apply records completion
without changing workspace files.
ps, status, and doctor inspect saved apply state and existing worker
ownership without restarting workers. An unfinished apply with no active owner
is reported as needs recovery. This does not mean no files were changed:
a crash can occur after installation but before completion was recorded.
Explicit fetch --apply uses the transaction journal and current workspace
checks to resolve that state safely. Conflicts and permanent failures retain
their error details for review.
The default ps listing includes jobs needing apply recovery even after remote
execution finishes. Explicit peer, workspace, and --last filters still apply.
status can show local apply state when the runner is unreachable, while
reporting remote execution as unknown and returning a failure exit code.
When only local evidence is available, JSON omits unavailable remote receipt
fields rather than filling them with empty values. An expired job receipt is
shown in ps without failing a successful fleet listing or suggesting that
its missing changes can be fetched. An unreachable peer is queried once for
its listing; local recovery information does not trigger further requests there.
doctor warns about interrupted applies across this client's saved jobs.
JSON reports the same apply state without extra diagnostic chatter on stdout.
These reports reflect the state observed when each command runs.
[run].peer chooses a preferred configured peer for new runs. --on or
--url wins over this preference; an unknown workspace alias fails instead
of silently using the personal default. errand config shows the effective
run settings and their sources; errand config --json provides the same
information for scripts. Inspection stays local and does not resume automatic
applications. Named profiles in personal or workspace config can supply peer,
workdir, and apply preferences: errand --profile build -- go test ./....
CLI flags override profiles; errand config --profile build explains the
result. See configuration for syntax and precedence.
Available since v0.1.2. Run a command in a separate workspace on this machine:
errand setup --local
errand --on local -- make testThe command receives a snapshot with the same file-selection rules as a remote job. Its ordinary workspace edits stay in the job; your original checkout changes only through apply. The process still has your account's permissions, so this is a workspace copy, not a security sandbox.
Start attached, press Ctrl-D if you want to stop watching, then use the
printed local/JOB_ID handle:
errand ps --on local --last 5
errand attach local/JOB_ID
errand fetch local/JOB_ID
errand fetch --apply local/JOB_IDExit codes, retained artifacts, named caches, explicit -d, and --apply /
--no-apply follow the same rules as remote jobs. Each new invocation starts
a new job workspace; attaching resumes observation, not execution in a
completed workspace. Nothing automatically falls back to local execution.
For a local development server, connect directly to its listening port.
Local host jobs share your machine's ports; if you use forwarding, choose a
different local port, such as -L 8080:3000, to avoid binding the server's port.
See local-only setup and
local target configuration.
Ordinary runs use a fresh, ephemeral workspace. To keep files across jobs,
create a persistent workspace explicitly, then select it with --workspace
or an explicit profile:
errand workspaces create --on mac-mini experiment
errand --on mac-mini --workspace experiment -- make test
errand --on mac-mini --workspace experiment -- make test
errand workspaces --on mac-mini
errand ps --on mac-mini --workspace experiment --last 5
errand df --on mac-mini --verbose
errand workspaces rm --on mac-mini experimentCreation uploads your current working files using the usual snapshot selection
and configuration rules. create --no-snapshot starts empty instead. A missing
workspace name on a run is an error; it never creates a workspace implicitly.
Names start with a letter and contain up to 64 letters, digits, hyphens, or
underscores; names that look like workspace IDs are reserved. They are scoped
to your authenticated identity on that runner.
Subsequent runs use the files already there, at the same runner-side path. They do not upload your latest local edits. Each command gets its own job handle, logs, exit status, and retained results. Detaching and attaching work as usual. The workspace survives failed commands and daemon restarts; interrupted commands are not replayed. Multiple jobs can use the same workspace, within the runner's normal concurrency and queue limits. For example, keep a development server running while another job runs tests. Killing or finishing one job leaves the others running. Removal is refused until every job has finished cleanup, including queued jobs.
Jobs share the actual files and cache directories. Errand does not serialize
file writes or determine whether commands are read-only; use separate workspaces
when commands need independent files. workspaces and df --verbose show every
job currently holding the workspace.
Job handles still support fetch, fetch --apply, fetch --apply --conflicts,
and fetch --output. Each job's retained result is an immutable comparison with
the workspace's creation snapshot, including accumulated edits from earlier
jobs. Applying it uses the originating directory and the conflict-safe merge rules
described below. Fetching an old result neither updates
nor deletes the live workspace. Use --no-apply when you want to keep iterating
there without applying job results locally. With concurrent writers, a retained
result may include a sibling's edits. Collection checks the files it reads and
can fail if they change during capture; it is not an atomic workspace snapshot.
For a stable final result, wait for writers to finish, then run a final command
such as errand --workspace experiment -- true and fetch that job.
Keep the server on your runner, edit locally, and push changes to the same
workspace. For a pnpm project, add a profile to .errand.toml:
[profiles.dev.run]
peer = "mac-mini"
workspace = "app-dev"
workdir = "."From the project root, create the workspace and prepare the application once:
errand workspaces create --profile dev app-dev
errand --profile dev -- pnpm install --frozen-lockfile
errand --profile dev -- pnpm build:packages
errand --profile dev --forward 3000 -- pnpm devUse your project's build and development commands and actual server port.
Omit build:packages if your project does not need a separate shared-package
build. If the application needs local environment files, add their paths to the
profile before starting it:
[profiles.dev.env]
files = [".env.local"]Keep secret files excluded with .gitignore or .errandignore. Files are loaded
explicitly; env.pass remains available for exported shell variables.
See environment files for precedence
and supported syntax.
Dependencies and build outputs remain in the persistent tree between jobs,
including ignored files. Named caches are optional for this loop.
Leave the server attached in that terminal and open http://localhost:3000
in your local browser. In a second terminal, edit files in the same checkout:
errand push --profile dev --applyPush delivers the local source changes into the running workspace. The
application's existing file watchers can rebuild and hot reload without a
server restart. Push completion confirms file application, not application
readiness; check the server output and browser. If shared-package edits need
a separate build, run errand --profile dev -- pnpm build:packages after push.
Restart the development command when the application requires it.
Runs do not upload edits automatically. Plain push stages without applying;
--apply remains explicit even if the profile enables automatic result apply.
Push uses three-way merging and refuses conflicts by default. Use a separate
workspace when you need independent files for another development session.
Keep the development server in its terminal, and run this in another:
errand push --watch --apply --profile devWatch performs an initial push and repeats it when selected local files change.
--watch controls repetition; --apply still controls whether the running
workspace is updated. push --watch continuously stages changes without applying
them. All other push options retain their meanings, including a selected changed
PATH, --include-all, workspace and peer overrides, and profiles. A selected
path with no changes is reported as unchanged and watch keeps waiting.
Save bursts are batched with a short quiet period. Only one push runs at a time;
edits arriving during it are collected for the next pass. The display shows
watching, sending, pending edits, and reconnecting, with the usual transfer
receipts after each push. On a terminal the status line updates in place; redirected
output uses plain lines. With --json, stdout contains newline-delimited push
receipts using the existing fields, while status messages go to stderr. Idle
watching produces no transfer receipts. Completion confirms file application,
not a successful build or application readiness.
Ctrl-C stops watching and waits for an in-flight push to finish. The remote
application keeps running. A second interrupt forces exit; restarting push
recovers any uncertain apply before sending new changes. Transient connection
failures retry with backoff. Conflicts stop watch. --apply --conflicts first
applies clean changes and eligible text conflict markers, then stops and reports
the conflicts. Resolve the files explicitly before restarting watch.
Watch pins the workspace ID and checkout identity. A removed or replaced checkout, changed selection policy, or removed workspace stops the session. Reusing a workspace name cannot redirect an existing watch. Concurrent commands remain caller-managed writers; watch does not fetch remote edits or resolve application build dependencies automatically.
Watch sends changed source entries and compact metadata against the retained source checkpoint. Unchanged running files are never used to reconstruct source. The CLI and daemon must run the same version. Local native notifications use kqueue on macOS and inotify on Linux. Watch setup reports exhausted OS watch/file-descriptor limits as errors; it does not silently fall back to continuous full-tree polling.
Fetch addresses a job's fixed result. Push addresses the continuing workspace:
errand fetch --apply mac-mini/JOB_ID
# Edit locally, then inspect the staged transfer or apply it remotely.
errand push --on mac-mini --workspace experiment
errand push --on mac-mini --workspace experiment --apply
errand --on mac-mini --workspace experiment -- make testPlain push freezes and stages the selected local snapshot without changing the
runner's working files. --apply merges it into the workspace. Both directions
use clean-or-refuse three-way merging; --apply --conflicts explicitly permits
conflict markers and clean sibling changes. Existing markers are ordinary file
contents. There is no transfer --force or implicit
fetch before push. Concurrent commands remain caller-managed writers.
Push requires --workspace NAME or a selected profile's run.workspace, and the originating checkout on the machine that
created it. Job handles are fetch targets, not push targets. Workspace names are
resolved to immutable IDs before transfer; deletion and recreation cannot redirect
an in-flight push.
Push supports --on, --url, --profile, --workspace-root, --include-all,
--json, and an optional complete changed PATH to limit application. Staging
describes the complete selected snapshot, but reuses verified file contents in
the runner's snapshot cache and uploads only missing bodies. Workspace creation
and successful uploads populate that cache. A cold or disabled cache requires all changed bodies; eviction or corrupt cached content
during transfer safely retries the same frozen snapshot. Cache cleanup failures
do not prevent that retry. The optional PATH limits application, not snapshot
selection. Fetch downloads the retained change bundle. Push uses the normal snapshot
selection rules and the workspace's fixed cache bindings. Artifact declarations
retain remote outputs; they do not opt ignored local files into an upload.
Changing snapshot policy requires a new workspace. Apply is explicit even when
run configuration specifies changes.apply_on_success = true.
For persistent workspaces, fetch/apply tracks the last accepted
remote source, and push/apply tracks the last accepted local source independently.
Destination-only edits never become the merge base. A partially conflicted apply
advances clean paths while keeping the old base for conflicted paths. A later
job returning to the creation state can therefore undo previously accepted remote
changes, even when its creation-relative result is empty. Omitting an artifact
from a later job's retention policy does not delete an earlier fetched artifact.
Plain fetch and --output continue to expose that job's immutable retained result.
There is no direct live-workspace fetch command.
Repeating push after an uncertain apply response completes that exact request
before accepting another snapshot, including when repeated without --apply.
The CLI identifies the recovered push; run push again to send newer local edits.
Completed retries do not overwrite later job edits. If the runner collected a
stage before receiving its apply request, the client uploads the frozen source
again. After an ordinary staged push, push --apply uses the acknowledged stage
without another upload, or prepares a new transfer if local files changed. Repeating a successful
fetch/apply with the same job and selection replays its recorded outcome; use a
new job result to fetch subsequent remote work.
Push and fetch use the same summary format on stderr:
errand: push: applied 3 changed paths to workspace experiment on mac-mini; 12 KiB transferred in 0.42s
errand: fetch: staged 3 changed paths from mac-mini/JOB_ID; 8.0 KiB transferred in 0.31s
staged means files are available for inspection. applied means the file
application completed; it does not check application readiness or hot reload.
Fetch uses exported for --output. Recovered pushes are identified separately
and still require another push to send newer local edits.
Counts refer to changed paths, including directory changes represented by one root. Staging reports the staged roots (or the requested fetch selection), export reports the selected roots, and apply reports accepted roots from the application outcome. A repeated persistent apply can replay that recorded outcome. Conflicts exit nonzero and never print a success summary.
Bytes measure multipart transfer bodies sent or received during this invocation,
including archive framing and metadata, excluding HTTP headers and control
requests. They describe the transferred snapshot or bundle, even when PATH
limits application. Reusing a staged upload or cached download reports zero;
re-uploading a collected stage during recovery counts the new upload. Elapsed
time includes local preparation, transfer, and application.
Plain push still prints only its transfer ID on stdout; plain fetch and export
print only the resulting path. Both accept --json, with matching status and
transfer fields (changed_paths, transferred_bytes, elapsed_ms). Push keeps
its existing receipt fields; fetch includes path. Transfer failures include
error, and conflicts include conflicts and materialized. Counts on failure
are partial and do not establish completion. Flag or target-resolution errors
are reported on stderr before a transfer report is available.
df includes local transfer storage under changes and remote transfer storage
under its workspace (df --verbose shows a transfers column). Use
gc changes --older-than 7d locally or
gc changes --on mac-mini --older-than 7d on a runner to collect old staging and
unreferenced source bodies. --dry-run previews collection. Accepted checkpoints,
creation source bodies, pending applications, and the latest apply receipt stay
protected so an uncertain response remains retryable. Removing a remote
workspace removes its remote transfer state, while local state remains available
for retained job results. Transfer staging is bounded by the existing change
byte limit and 4,096 attempts per relationship; collect old staging when full.
Definitively rejected workspace creation removes its local origin snapshot.
An uncertain creation outcome preserves that snapshot; check workspaces before
retrying. GC preserves damaged transfer state, continues collecting healthy
relationships, and reports partial progress with a nonzero exit status.
Transfer state for a workspace whose directory was moved or deleted is kept
and reported as skipped, not failed; GC collects it again if the directory
returns.
df reports incomplete inventory instead of waiting for a busy local transfer;
gc changes --dry-run counts such relationships as protected.
Concurrent inventory and dry-run reads do not mark one another as active transfers.
Uploads to the same workspace wait for the preceding upload to finish; commands
remain independent. df includes temporary upload bytes in workspace transfer
storage, including an upload whose target workspace has just been removed.
Whole source snapshots and retained source bodies use the workspace byte limit; individual deltas and attempt staging use the change byte limit. An unchanged large source file therefore does not consume the delta allowance. Historical source bodies still consume storage, and pinned bodies cannot be reclaimed by GC.
Profiles and configuration still provide peer, environment, workdir, forwarding,
and apply preferences. Persistent workspace selection requires the explicit
--workspace flag or run.workspace in an explicitly selected profile.
Creation freezes ignore rules and named-cache
bindings; restored cache directories stay live inside the persistent tree. Ignored
files generated inside the tree survive for later jobs, but returning them still
requires artifact declarations. Per-run artifact flags can override retention.
Creation-time artifacts are inherited during reuse. Explicit flags or a selected
profile can override them; later ambient config changes do not replace the
workspace's defaults. Explicit conflicting cache bindings are rejected.
Cache paths are real directories shared by members of that workspace. Changed
trees are saved when the final successful member returns. Independent workspaces
can use the same named caches concurrently, with the normal responsibility to
coordinate writes to shared hardlinked files. Errand does not rewrite embedded
absolute paths or tool configuration. See named caches.
df includes persistent workspace storage. Job and cache GC never collect the
persistent tree. workspaces rm explicitly removes an idle workspace and its
creation snapshot, leaving existing job receipts and retained results available
under their normal retention rules. Listing, creation, and removal accept
--json; all accept --on or --url and work with local, SSH, and tailnet peers.
df --verbose (-v) breaks workspace storage into working files, the creation
base, transfer data, and metadata. Named caches and job storage are listed separately, so
shared caches are never charged again to each workspace. Sizes are logical
regular-file bytes, not unique disk allocation; filesystem clones can share
physical blocks. Cache sizes reflect the last lease release, not live growth.
ps --workspace NAME filters jobs before the runner's listing limit. It keeps
the usual active-only default; add --all (-a) or --last N (-n N) for
history. The filter accepts an ID too, including after workspace removal while
receipts remain. Names select the current workspace: recreating a name does
not include jobs from the previous workspace. Across peers, missing names
match no jobs. --json includes each job's workspace_id.
An attached terminal can detach at any time with Ctrl-D and later resume with
errand attach HANDLE. Ctrl-C keeps its Unix meaning: it sends SIGINT to the
remote command, and a second Ctrl-C force-kills it. Interactive detachment
returns 0 for the detach action; it is not the unfinished job's exit status.
Non-terminal EOF is ignored, so scripts remain attached unless they request
--detach explicitly.
To read a job's output without following it, add --no-follow: attach prints
what the job has written so far and exits 0, leaving the job alone, with no
Ctrl-C forwarding and no workspace apply. That answers "did the service start?"
from a script or an agent:
errand attach --no-follow --tail 100 gb10/JOB_ID
errand attach --no-follow --since 10m gb10/JOB_ID | grep "socket connected"--tail N starts with the last N lines, counted across stdout and stderr in
the order the runner saw them, and --since skips output written before a
duration ago (10m) or an RFC 3339 time. Both also work on a followed
attach, which then continues live from there.
--forward [LOCAL:]REMOTE opens TCP listeners on IPv4 and IPv6 local loopback for the
attached session. It is repeatable and can be added when initially running a
job or on any later attach. Omitting LOCAL uses the remote port locally.
The runner end connects to 127.0.0.1:REMOTE, then [::1]:REMOTE, on the
runner itself, so a service bound only to loopback is reachable and should stay
that way. Binding 0.0.0.0 is never needed for forwarding and exposes the port
to everything that can reach the runner. A connection fails with "connecting to
job port" when nothing listens on runner loopback at that port.
Ctrl-D closes the attachment's listeners and active connections without
stopping the job. Forwarding is not remembered, and --detach --forward is
rejected because no attached client would remain to own the listener. Host
jobs share the runner's loopback network, so Errand cannot prove that a
listening service belongs to the selected host job. Capability-based runners
require the separate forward-own action. Forwarded connections carry bytes
in both directions while open; EOF or reset on either side closes that
connection.
After every command, Errand compares the final remote workspace with the
submitted snapshot and retains every snapshot-representable created, modified,
or deleted path. No declaration is required. Every submitted path is compared.
New paths are retained only when allowed by the ignore policy frozen during
snapshot selection, so a remote command cannot widen retention by rewriting
.errandignore or .gitignore. Git metadata, Errand apply transactions, and
transient filesystem nodes such as sockets are excluded.
Failed commands retain their changes too,
which makes reports, traces, and partial build products available for
inspection. A --no-snapshot job starts from an empty baseline, so every file
it creates is retained.
A run reports retained changes and the job handle but does not download them
unless --apply or apply_on_success requests automatic application after a
completely successful transaction. The policy survives explicit --detach
and interactive Ctrl-D; failed commands never apply automatically. The
originating client records background application progress and errors, which
errand status HANDLE reports alongside the changed paths. errand attach HANDLE follows logs and status only; it never downloads or applies workspace
changes. errand fetch HANDLE [PATH]
stages all changes or any retained path. Applying a selected path requires a
retained change root or an ancestor of one; errand fetch --apply HANDLE [PATH]
performs a three-way merge from the submitted snapshot, the current local
workspace, and the completed remote workspace. Errand applies the result only
when the entire selected merge is clean. On conflict, the working tree remains
untouched and the staged base and remote trees remain available.
errand fetch --apply --conflicts HANDLE [PATH] explicitly chooses a
non-clean state: clean changes are applied, text conflicts receive standard
conflict markers, and binary or type conflicts keep their local values. It
exits nonzero, reports unresolved paths, and keeps the base and remote values
in staging for manual resolution. Deletions use the same merge rules;
fetching only deleted paths returns the retained bundle.json tombstone
metadata because no file exists to return. A different machine can fetch
and inspect changes, but cannot apply them without the originating machine's
workspace identity record. Change records are scoped by runner endpoint and
job ID, so one peer cannot reuse another peer's apply state.
Use errand fetch --output DIR HANDLE [PATH] (short form -o DIR) to copy
retained remote files into a directory of your choice. For example,
errand fetch -o ./results HANDLE reports/test.html creates
./results/reports/test.html. Paths stay relative to the submitted workspace;
the export contains remote values, without the staging metadata or base tree.
The parent directory must exist and DIR must not exist, even as an empty
directory or symlink. Errand verifies the selected contents before publishing
the complete directory without overwriting an existing destination.
Exports work for failed jobs and from a different client machine. They do not
merge into the originating workspace or mark changes as applied, and cannot
be combined with --apply or --conflicts. Selecting a deleted path has no
remote value to export; use plain fetch to inspect its deletion metadata.
Runs summarize configured caches and artifacts on one line. Use --verbose
(or -v) before -- to show each cache binding and retained artifact path.
To retain generated outputs that Git or .errandignore excludes, declare exact
workspace-relative files or directories:
# .errand.toml
[artifacts]
paths = ["reports", "dist/app"]Or use errand --artifact reports -- make test; repeat --artifact for multiple
paths. --no-artifacts clears configured declarations. Declarations select
remote outputs without uploading local ignored files. Failed jobs retain their
outputs too; missing outputs are allowed. Fetch, export, and apply use the same
retained bundle and conflict rules. See artifact configuration
for profiles, precedence, and limits.
To reuse disposable build data on a runner, use --cache compiler=target or
configure [caches] with compiler = "target". --no-caches clears configured
bindings. Cache contents stay on the runner and are excluded from both input
snapshots and fetched results. Every binding uses the same directory-preservation
mechanism. For example, [caches] with dependencies = "node_modules" lets a
separate errand -- pnpm install supply installed files to a later
errand -- pnpm test. Monorepos need explicit bindings for nested installed
directories too. Directory groups
can declare those package caches together. See named caches for concurrency, hardlink
behavior, path-portability limits, recovery, and cleanup.
Git is not required for non-Git snapshots, running jobs, status, logs, or plain
fetches. Applying changes needs git merge-file on the client only when both
the local and remote sides changed the same text file and a true three-way text
merge is required. Missing Git fails that apply safely before installation.
A job is a plain process on the runner, not a login session. It gets no
terminal and no stdin, and the runner does not source shell profiles; use
sh -lc '...' when a command depends on them. Its environment starts from a
small allowlist of the runner's own variables, then adds declared variables and
ERRAND_JOB_ID. When the snapshot came from a Git commit, ERRAND_GIT_COMMIT
and ERRAND_GIT_DIRTY are set too; --no-snapshot jobs, non-Git directories,
and repositories without a commit don't get them.
On macOS and Linux runners the allowlist is PATH, HOME, USER, LOGNAME,
LANG, and TMPDIR, plus XDG_RUNTIME_DIR and DBUS_SESSION_BUS_ADDRESS when
the runner has them. On Linux, errand setup runs the runner as a systemd user
service, whose manager sets XDG_RUNTIME_DIR (and, on most distributions, the
user bus address), so jobs can use systemctl --user directly. Windows runners
pass Windows variables instead, such as USERPROFILE, APPDATA, TEMP,
SystemRoot, ComSpec, and PATHEXT, and none of the Unix-only names above.
Jobs share the runner's network, including its loopback. Bind services to
127.0.0.1 and reach them with --forward (see above).
Every ordinary run starts in a fresh workspace that is removed after the job finishes, so a restarted service starts from the files you sent. Keep state that must survive restarts, such as a SQLite database, either in a persistent workspace or in a runner path outside the workspace that the service is configured to use. To avoid reinstalling dependencies on every restart, declare them as a named cache. For npm:
# .errand.toml
[caches]
dependencies = "node_modules"errand --on mac-mini -- npm install --prefer-offline --no-audit
errand --on mac-mini --detach -- npm startInstall in its own job, and run it again when dependencies change. A cache is saved only when a job exits 0, and a service usually ends by being killed, so anything a service job installs into the cache is discarded.
npm ci deletes node_modules before installing, so it discards a cached
tree; npm install keeps it and only installs what changed. npm's own
download cache in the runner's ~/.npm is reused between jobs either way.
Bare errand ps queries every explicitly configured peer, merges the results
newest-first by job ULID, and shows active jobs only. The active filter is
applied by each runner before its bounded receipt window, so retained terminal
jobs cannot hide a long-running job. --all includes terminal receipts;
--last N includes all states and applies one global limit after merging.
--on and --url explicitly narrow either view to one runner. Bare
errand peers, errand df, and errand workspaces follow the same
all-configured-peers rule.
df groups local runner storage and fetched changes into one local row.
df --verbose (-v) adds individual workspace, named-cache, and job storage
tables. df --verbose --json includes the corresponding details object;
runners must return the requested inventory details.
Remote rows also include changes fetched by that runner's OS account. See storage maintenance
for accounting and cleanup details.
These read-only fleet commands share target selection, concurrent querying,
partial-failure reporting, and exit semantics. Commands that mutate runner
state remain explicitly single-runner.
Interactive terminals show wrapped job cards with complete commands. Piped or
redirected output uses a compact plain table for tools such as grep and
head. Both forms show admission and process start times, process duration,
project, source snapshot, command, and terminal outcome. WORKDIR appears only
when a listed job runs below its workspace root. Git sources are shown as a
short commit plus +dirty when applicable; errand ps --json retains exact
structured project, workdir, commit, and manifest metadata, including
truncation flags. Running durations are measured on the runner, so caller and
runner clock offsets do not distort them.
errand status HANDLE shows one job's complete human-readable execution view:
runner, state, command, timing, process and transaction outcomes, retained-log
availability, retained workspace changes, and relevant next
commands. errand status --json HANDLE emits the same owner-visible data as
structured JSON. ps remains the multi-job listing; attach, fetch, and
kill remain the actions on a selected job.
Client-side workspace identities, downloads, and apply transactions are
stored under $XDG_STATE_HOME/errand, or ~/.local/state/errand when
XDG_STATE_HOME is unset. XDG_STATE_HOME must be absolute and can point to a
writable private location in restricted agent environments. Errand avoids
changing an already-secure directory. It attempts to tighten excess read access
but only refuses to proceed when ownership, missing owner access, or permissions
writable by another user would make the state untrustworthy.
The remote job states have deliberately narrow meanings:
staging: admitted and preparing its workspace, but the command has not started.queued: staging is complete and the job is waiting for a running slot. Its process start and duration remain blank.running: the command has started and has not reached a terminal result.exited: the command reached a terminal result, including ordinary nonzero exits and signals forwarded to or raised by the process.killed: Errand terminated the job because of a user request or enforced limit.ambiguous: Errand cannot prove a clean terminal outcome, commonly after restart reconciliation or a receipt persistence failure. It never silently replays the command.
detached is a local attachment condition, not a remote job state. Detaching
with Ctrl-D leaves the job unchanged. Ctrl-C sends SIGINT, and a second Ctrl-C
sends SIGKILL. errand kill HANDLE requests graceful SIGTERM termination;
errand kill --force HANDLE sends SIGKILL. Staging and queued jobs can be
cancelled durably before they start. A submission is rejected as busy only
when both the configured running slots and bounded queue are full.
For commands that can run on several configured machines, use --where:
errand --where 'os=linux,kvm' -- ./run-vm-tests
errand --where 'go' -- go test ./...Errand filters for the required capabilities, chooses by available job capacity,
and prints the selected peer. The returned job handle works with attach,
fetch, and other job commands as usual. Use --on when you want a specific
machine, including when continuing a persistent workspace.
Requirements also work in workspace defaults and profiles. See automatic runner selection for syntax, selection rules, local opt-in, and retry behavior.