# Using Errand Run commands from a local workspace on a configured runner. For installation and the first run, start with the [README](../README.md). ## Command conventions 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`. ## Exit status 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. ## Workspace selection and run preferences 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: ```toml # .errand.toml [workspace] root = true [run] peer = "mac-mini" # optional; alias lives in personal config [changes] apply_on_success = true ``` Errand 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](CONFIGURATION.md) for syntax and precedence. ## Local jobs Available since v0.1.2. Run a command in a separate workspace on this machine: ```sh errand setup --local errand --on local -- make test ``` The 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: ```sh errand ps --on local --last 5 errand attach local/JOB_ID errand fetch local/JOB_ID errand fetch --apply local/JOB_ID ``` Exit 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](OPERATIONS.md#local-only-setup) and [local target configuration](CONFIGURATION.md#local-targets). ## Persistent workspaces 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](CONFIGURATION.md#profiles): ```sh 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 experiment ``` Creation 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. ### Persistent development loop 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`: ```toml [profiles.dev.run] peer = "mac-mini" workspace = "app-dev" workdir = "." ``` From the project root, create the workspace and prepare the application once: ```sh 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 dev ``` Use 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: ```toml [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](CONFIGURATION.md#local-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: ```sh errand push --profile dev --apply ``` Push 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. ### Watch local edits Keep the development server in its terminal, and run this in another: ```sh errand push --watch --apply --profile dev ``` Watch 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. ### Send local edits into a persistent workspace Fetch addresses a job's fixed result. Push addresses the continuing workspace: ```sh 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 test ``` Plain 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. ### Transfer output Push and fetch use the same summary format on stderr: ```text 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](NAMED_CACHES.md). `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`. ## Attached sessions and forwarding 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: ```sh 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. ## Retained changes, fetch, and apply 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. ## Artifacts and caches 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: ```toml # .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](CONFIGURATION.md#artifact-declarations) 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](NAMED_CACHES.md#directory-groups-for-monorepos) can declare those package caches together. See [named caches](NAMED_CACHES.md) 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. ## What a job runs with 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](#persistent-workspaces) 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](NAMED_CACHES.md). For npm: ```toml # .errand.toml [caches] dependencies = "node_modules" ``` ```sh errand --on mac-mini -- npm install --prefer-offline --no-audit errand --on mac-mini --detach -- npm start ``` Install 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. ## Job state and control 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](OPERATIONS.md#storage-and-garbage-collection) 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. ## Choose a runner by requirements For commands that can run on several configured machines, use `--where`: ```sh 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](CONFIGURATION.md#automatic-runner-selection) for syntax, selection rules, local opt-in, and retry behavior.