scribe-runner CLI reference
Documentation/CLI reference
scribe-runner CLI reference11 min read

scribe-runner CLI reference

On this page

Executes Scribe flow jobs for a web API.

scribe-runner is one binary with the Scribe client compiled in. It is started with three things — the web address, the runner's id and an API token — checks in with the web API, asks it for work, and executes each job in a workspace it keeps per repository and branch. It needs no scribe executable on the machine and reads no configuration file; every setting is an argument or an environment variable.

The web registration guide assumes scribe-runner is installed and links to no download. Take the scribe-runner-<platform> archive from the manual build workflow, or build it with scripts/install.sh (scripts/install.ps1 on Windows) beside the other executables. scribe-runner has no --json; --log-format json formats its logs.

See flows for definitions, runs, runner registration and the runner's behaviour (workspaces, shells, environment, stopping), and the web interface guide for the Flows tab.

scribe | scribe-server | scribe-auth | scribe-ctl | scribe-gateway

Reading this reference

Command synopses below come from this checkout’s built --help output. Angle brackets denote values, square brackets optional arguments. [OPTIONS] includes the logging options below and the command's own options. Logging options work before or after a subcommand.

Run scribe-runner --help for the command list, scribe-runner COMMAND --help for command help, and scribe-runner --version for the build version.

Commands

text
Usage: scribe-runner [OPTIONS] <COMMAND>

Commands:
  run      Run in the foreground; Ctrl+C finishes the current job first, a second one aborts it
  service  Install or remove the service that runs this runner with the machine
text
Usage: scribe-runner service [OPTIONS] <COMMAND>

Commands:
  install    Install as a service that starts with the machine (systemd, launchd or the Windows service manager)
  uninstall  Stop and remove the service installed for a runner

Runner options

run and service install take the same runner options:

Argument or optionMeaning
--server <URL>Web address, as in the browser: https://scribe.example (or SCRIBE_RUNNER_SERVER)
--runner <ID>The runner's id from its registration (or SCRIBE_RUNNER_ID)
--token-file <FILE>Read the API token (runners:write and flows:read) from a file; without it, SCRIBE_RUNNER_TOKEN
--workdir <DIR>Where workspaces, scripts and logs live (default: the user configuration root, runner/; or SCRIBE_RUNNER_WORKDIR)
--ca <FILE>CA certificates (PEM) that verify an https:// address instead of the public roots; repeatable
--poll-interval <SECONDS>Seconds between asks for work while idle [default: 5]
--keep-workspaces <COUNT>Branch workspaces kept per repository; the least recently used beyond this are removed [default: 8]

The address is the one the browser uses, with any path prefix the deployment serves the web interface under; the runner appends /api/v1. An https:// address is verified against the public web roots, or against the --ca files alone when any are given, so a private issuer is one flag (as scribe --tls-ca works); HTTPS_PROXY is honoured. The client offers the hybrid post-quantum key exchange X25519MLKEM768 first and also accepts classical X25519, which a proxy in front of the web API may offer alone. http:// is accepted only for a loopback host (localhost or a loopback IP such as 127.0.0.1 or [::1]); any other plaintext address is refused:

text
scribe-runner: "http://scribe.example" is plaintext on a network address; use https:// (http:// is accepted only for localhost)

The token comes from --token-file, or from SCRIBE_RUNNER_TOKEN when no file is named; there is no option that takes the token itself, because process listings show arguments to every account on the machine. It must carry runners:write and flows:read, and its owner must be the runner's owner or a Maintainer of every repository the runner serves. It is never logged and never passed to a step (see Trust boundary). The token is for the web API only; it never reaches a storage server.

The runner also has its own device key, device-key.pem, created on first use in a private file (mode 0600 on Unix): in the directory of the --token-file, or in runner-credentials/<id>/ under the user configuration root when the token comes from SCRIBE_RUNNER_TOKEN. Only its public key leaves the machine. At check-in the runner sends that public key, and the web API has auth derive a credential for it from the token's automation credential (reader ceiling, same account and expiry, revoked with the token) and returns the credential certificate with the storage and auth endpoints and their CAs. The runner fetches its access certificates from auth itself and keeps every certificate in memory. Those CAs are all it trusts: a CA handover is followed in memory, and after a job that met any other CA change the runner checks in again for the CAs the API delivers now (see CA rotation and handover). A web deployment without a TLS auth service configured answers check-in with 409 runner_auth_unconfigured, which the runner logs while it keeps retrying. On Unix a token file or device-key.pem that other accounts can access is refused ("… is accessible to other accounts (mode 644); run chmod 600 on it"), as is one in a directory other accounts can write that is not sticky; on Windows one whose access list lets another account read it, or whose directory lets another account delete entries, is refused the same way. See Registration and tokens and identity.

The work directory holds workspaces/ (one directory per repository and branch, or per workspaces: folder a flow declares, and workspaces.json describing them; each is registered on the server with the label <runner name>@<host> and the tags runner, runner-id=<id>), jobs/ (scripts of the job in progress, and the archives its bundles are packed into before upload), logs/ for a service installation, and scribe-runner.pid while a runner is up. It never holds the token or the device key: a service installation keeps both apart, under runner-credentials/<id>/ in the user configuration root. It is locked while a runner uses it; a second runner on the same machine needs its own. Without --workdir it is runner/<id> under the user configuration root (~/.config/scribe, ~/Library/Application Support/Scribe, or %LOCALAPPDATA%\Scribe); put a build machine's on the disk with room for its projects.

Bundles

A job whose flow declares bundles: ends with a Publish bundles step. The runner resolves the rules against the workspace, walking only the directories the rules can reach, packs archive targets into stored (uncompressed) zips under jobs/, and uploads three files at a time in parts of the size the API names, resuming after a lost connection from the byte the API has. Each file is hashed (BLAKE3) as it is read for sending and the digest goes with its last part, so the web can verify what it stored. The step's log says what matched, what was packed and how long each upload took; a rule that matches nothing is noted, not an error. The job's timeout-minutes covers publishing. See Bundles for the rule syntax and retention.

Logging options

Argument or optionMeaning
--log-level <LEVEL>off|error|warn|info|debug|trace (default info)
--log-filter <FILTER>Target filter, e.g. 'info,scribe_client=debug' (or SCRIBE_LOG)
--log-format <FORMAT>text|json
--log-dir <DIR>Write daily log files here instead of stderr
--log-keep <COUNT>Daily log files kept in --log-dir (default 14)
-v, --verbose...-v for debug, -vv for trace
-q, --quietErrors only
--timingsReport nested phase timings to the diagnostic sink

The runner's own diagnostics (check-in, claims, job outcomes, failures to reach the API) go to stderr or to daily files. A step's output is not diagnostics: it goes to the run's log in the web interface, not to the runner's log, streamed as the step writes it over one request held open per step (reopened after a silence, a lost connection, a megabyte or five minutes, resuming where the API confirmed). What a step wrote while the API was unreachable is kept, up to 8 MiB, and sent when it is back.

run

text
scribe-runner run --server https://scribe.example --runner <id> --token-file /etc/scribe-runner/token

Runs in the foreground until stopped. Ctrl+C or SIGTERM lets the job in progress finish, then exits; a second one aborts the job, which is reported failed with "the runner was stopped". A runner that cannot reach the API, is disabled, or whose token is rejected logs why and keeps trying at a slower pace; it never exits over a state the API named, so it picks up as soon as the cause is fixed.

While the runner is up — started this way or by a service — the root of its work directory holds scribe-runner.pid with the process id, one line, and the file is removed when the runner exits. It answers "is a runner using this directory, and which process" without a process listing; a file that is still there while no runner runs was left by a crash, and the next start says so as it replaces it. The work directory is also locked while a runner uses it, so a second runner started on the same directory is refused rather than sharing it.

service install

text
scribe-runner service install --server https://scribe.example --runner <id> --token-file FILE [--workdir DIR] [--system [--user NAME]]
OptionMeaning
--systemA system-wide service rather than one for the current user (needs administrator rights)
--user <NAME>The account a system service runs as (systemd only)

SCRIBE_RUNNER_TOKEN in place of --token-file works too. Checks the registration and token in with the API first, so a typo is a message rather than a service that restarts forever. Then copies the token to runner-credentials/<id>/token under the installing user's configuration root (~/.config/scribe, ~/Library/Application Support/Scribe, or %LOCALAPPDATA%\Scribe), in a directory and file readable by that user only and outside the work directory, and registers a service that runs scribe-runner run with the same options and --token-file pointing at it. The service's device key is runner-credentials/<id>/device-key.pem: the installing check-in creates it there (not beside the --token-file given), so the key the web API had certified is the one the service uses. The command prints the service name, the runner, the unit file, the stored token and the work directory. A --workdir that contains that directory, or lies inside it, is refused.

  • Linux: a systemd unit, ~/.config/systemd/user/scribe-runner-<id>.service or with --system /etc/systemd/system/…, enabled and started with systemctl [--user] enable --now. A user service stops with the login session unless loginctl enable-linger is on; the command says so. The unit restarts the runner after a failure, and its stop timeout is an hour so a running job can finish.
  • macOS: a launchd agent, ~/Library/LaunchAgents/com.flyingrat.scribe-runner-<id>.plist, or with --system a daemon under /Library/LaunchDaemons, bootstrapped at once and kept alive; output goes to <workdir>/logs/launchd.log and the runner's own daily files beside it.
  • Windows: a service created with sc.exe, started automatically at boot, logging to <workdir>/logs. It runs as LocalSystem; to build under a user account, sc.exe config scribe-runner-<id> obj= .\user password= …. The service manager starts the executable with the hidden service entry command.

The service name is scribe-runner- followed by the first eight characters of the runner id. Installing again for the same runner replaces the service.

service uninstall

text
scribe-runner service uninstall --runner <id> [--system]

Stops and removes the service installed for the runner (--system when it was installed that way), then deletes the token service install stored and its runner-credentials/<id> directory, device key included. The work directory and its workspaces are left in place; remove them by hand when the machine is done with them. Removing the runner's registration in the web interface is separate.

Environment

VariableMeaning
SCRIBE_RUNNER_SERVERDefault for --server
SCRIBE_RUNNER_IDDefault for --runner
SCRIBE_RUNNER_TOKENThe token, when --token-file is not given; the device key is then kept in runner-credentials/<id>/
SCRIBE_RUNNER_WORKDIRDefault for --workdir
SCRIBE_LOGDiagnostic filter, as for every Scribe executable
SCRIBE_TRANSFER_JOBSParallel object transfers during checkout, 1–8 (default 4)

Trust boundary

A runner executes code written by anyone who can write the flows of a repository it is bound to: every run: step is such code, running as the runner's OS account. Treat binding a runner to a repository as giving that repository's Writers a shell on the machine, under that account.

  • Steps get no runner credentials. A step starts from an empty environment plus PATH, HOME, USER, LOGNAME, SHELL, LANG, LANGUAGE, LC_*, TZ, TERM, TMPDIR, TEMP and TMP, and on Windows SystemRoot, SystemDrive, windir, ComSpec, PATHEXT, OS, USERNAME, USERPROFILE, HOMEDRIVE, HOMEPATH, APPDATA, LOCALAPPDATA, ProgramData, ProgramFiles, ProgramFiles(x86), ProgramW6432, CommonProgramFiles, CommonProgramFiles(x86), CommonProgramW6432, NUMBER_OF_PROCESSORS and PROCESSOR_ARCHITECTURE; then the run's variables and the flow's and job's env. Nothing else the runner was started with reaches a step — not SCRIBE_RUNNER_TOKEN, not another SCRIBE_* setting, not a cloud or signing key the operator exported. A build that needs a host variable sets it in the flow's env.
  • The account is the boundary. Job steps run as the runner's OS user, so a step can still read every file, and inspect every process, that account can: the token file, device-key.pem, the runner's own environment (/proc/<pid>/environ, ps eww) and memory, the account's SSH keys and caches. Keeping the token and the key out of the work directory and out of step environments stops them leaking by accident, not a step written to find them. Run runners under a dedicated low-privilege OS account that holds nothing else of value, and do not reuse a developer's or an administrator's account.
  • Choose the token's account for what it exposes. The runner's token and its device key can be taken by such a step. Whoever holds them can read what the token's owner can read (the derived credential's ceiling is reader), and can act as that owner's runners — claim their jobs, report results and upload bundles — for the repositories it reaches. Use the token of an account (a shared CI account, for instance) whose read access you are comfortable exposing to the Writers of every repository bound to runners it drives, and bind runners of one token only to repositories with the same set of trusted Writers.

Exit status

0 after a clean stop; 1 for an argument, installation or startup failure, reported on stderr. A job's failure is reported to the web API, not through the exit status: the runner keeps running.

Source docs/reference/cli/scribe-runner.mdSnapshot 2b8dcccb