Getting Started
This chapter takes you from a clean checkout to a running agent: prerequisites, per-OS setup, building, API keys, your first run, and the full CLI flag reference.
Fresh box in one command
Standing up a new machine end to end (clone, dependencies, build, launch,
and a Connect discovery link) is one command — see
Credential Custody for
the trust story behind it. The canonical installer is a release asset:
every v* release attaches install.sh / install.ps1 stamped with the
release’s tag and commit, with their sha256 hashes committed to the public
transparency log
alongside the binaries’ — immutable once published, and outside any hosted
serving path:
# macOS / Linux
curl -fsSL https://github.com/intendant-dev/Intendant/releases/latest/download/install.sh | sh
# Windows (PowerShell)
& ([scriptblock]::Create((irm https://github.com/intendant-dev/Intendant/releases/latest/download/install.ps1)))
To link the daemon to a rendezvous for discovery, say so explicitly —
… | sh -s -- --connect https://intendant.dev (or your
self-hosted rendezvous); the installer no
longer learns a default from wherever it was fetched. Add --service /
-Service on an unattended box: intendant service install
registers the daemon with the platform’s native supervisor (systemd where
present, launchd on macOS, Task Scheduler on Windows, cron @reboot plus a
built-in restart supervisor on systemd-less Linux — no init system is a
dependency) and prints where the one-time claim code lands. intendant service uninstall|status manage it afterwards.
The installer stands up its own prerequisites: when git is missing (stock
Debian minimal cloud images ship without it), the pre-flight installs it
through the platform package manager — apt/dnf/yum/zypper/pacman/apk/brew,
winget or choco on Windows — announcing the exact command before it runs,
escalating via sudo only where it exists and never silently, and otherwise
stopping to name the command for you to run. Rust and the native build
dependencies follow the same law via the per-OS setup scripts from the
cloned tree.
For reproducibility, pin the tag instead of latest:
…/releases/download/v0.1.0/install.sh. Release assets are immutable, and a
stamped installer double-checks itself: it prints its release identity
(tag @ commit) before doing anything else, installs exactly that released
tree, and fails closed with RELEASE_PIN_MISMATCH when the checkout doesn’t
match the commit its release recorded. Everything else it executes comes from
that verified tree, and cargo build --locked extends the pinning to
dependency hashes.
Pick your rung of the trust ladder honestly:
- The one-liner is the convenience rung: you are trusting GitHub’s release storage and the transparency log’s public evidence, sight unseen.
- Download, read, then run the same asset — it is a short script.
Check it against the release’s
install.sh.sha256and the logged release manifest first. - A signed native app outranks both once published. No Developer
ID-signed/notarized release exists for this alpha, and an
-unsigned-devartifact never qualifies as an anchor.
https://intendant.dev/install.sh survives only as a non-canonical
convenience: it answers with a redirect (HTTP 302) to the release asset and
never a script body — script against the GitHub URL, not the pretty one.
While no v* release has been published yet (the current state of this
alpha), the asset URL 404s; until the first release, clone and build from
source — the prerequisites and build steps below are
exactly what the installer automates.
What happens next is the whole story in four steps:
- Link. The daemon prints a single-use twelve-word claim code in its log. Enter it at your rendezvous — intendant.dev/connect for the hosted service (invite-only during the alpha), or your self-hosted rendezvous — and the route appears under Your computers. Linking is discovery metadata only: it creates no IAM principal or grant and grants no daemon access.
- Establish a trusted owner. On the machine’s console or SSH session, run
intendant access setup; alternatively use a direct mTLS connection. The packaged macOS app contains a local mTLS bridge, but no Developer ID-signed/notarized release has been published for this alpha, so its current-unsigned-devartifact is not an anchor. The default build cannot mint or exercise root through hosted-origin code. The former--owner <browser-key>shortcut is retired in this alpha; a bare fingerprint is not remote authentication. - Authorize on that trusted surface. Install/use the daemon’s browser mTLS client certificate or stay on loopback. Browser identity-key records are not an active login mechanism in this alpha. The hosted Connect tab remains discovery-only; there is no opt-in that turns it into a control client.
- Fuel and work. Open the trusted dashboard: until a credential lands, the
Activity greeting offers Add API keys and Sign in an agent buttons —
and when external coding agents are already signed in, it leads with a
“…ready to work” headline instead of a blocker. Grant a time-boxed
API-key lease (or relay provider calls through your browser), configure a
local key, sign an external agent into its subscription from the Vault tab,
or run
intendant auth chatgpt loginfor Intendant Native. API-key and native ChatGPT leases are memory-only, but this is not a blanket disk guarantee:.envand the optional private native ChatGPT login store are durable, while a full-credential OAuth lease for an external CLI temporarily materializes a private auth home under<state-root>/leased-auth(~/.intendant/leased-authby default) until cleanup. Credential Custody is the full story. Then send the first task from the composer, watch it in Activity, and dial autonomy and approvals to taste.
Upgrading an earlier alpha installed with
--owner: remove the retired flag from the service command, upgrade and restart the daemon, close any old Connect dashboard tabs, then runintendant access setuplocally and install the generated mTLS client credential. CLI parsing now rejects--ownerwith this migration direction. IAM schema v2 revokes bothconnect-bootstrapgrants and legacy browser-key grants whose reason records a--ownerbootstrap; it does not silently preserve browser-key root.
The rest of this chapter is the from-checkout path the installer automates —
including the classic alternative to fueling from the vault: putting an API key in .env on
a machine you fully trust.
Prerequisites
Intendant is a Rust workspace. At minimum you need:
- Rust 1.96.1 — pinned by
rust-toolchain.toml;rustupselects and installs it automatically - wasm-pack 0.14.0 — pinned by
.wasm-pack-version; install it withcargo install wasm-pack --version 0.14.0 --locked(the build auto-rebuilds stale browser WASM, see WASM) - ffmpeg — display recording and software H.264 encoding
- Provider credentials — an API key in
.envfor at least one of OpenAI, Anthropic, or Gemini; an Intendant-owned ChatGPT login; or nothing on disk at all, because an authorized daemon can run on vault leases
Platform-specific runtime dependencies (display capture, input injection, audio routing) are best installed with the setup script for your OS.
Per-OS setup scripts
The scripts in scripts/ install everything a fresh machine needs and verify
what is already present. Each accepts --check to report status without
changing anything.
# macOS — installs cliclick, ffmpeg, sox, SwitchAudioSource, wasm-pack,
# Vortex Audio HAL plugin (or BlackHole fallback), and builds the app bundle.
./scripts/setup-macos.sh
./scripts/setup-macos.sh --check # report only
# Linux (Debian/Ubuntu) — installs the APT_PACKAGES set: libvpx, libxcb +
# libxcb-shm + libxcb-randr, libpipewire, xdotool, imagemagick, ffmpeg,
# pulseaudio-utils, ripgrep, Xvfb, and toolchain build deps.
./scripts/setup-linux.sh
./scripts/setup-linux.sh --check
# Windows (Server 2022 / 11), PowerShell — see ./windows-support.md
./scripts/setup-windows.ps1
Manual Linux install if you would rather not run the script:
sudo apt install build-essential binutils pkg-config libclang-dev \
libvpx-dev libpipewire-0.3-dev libxcb1-dev libxcb-shm0-dev libxcb-randr0-dev \
xdotool x11-utils imagemagick ffmpeg xvfb pulseaudio-utils ripgrep xdg-utils \
ca-certificates libnss3 libatk-bridge2.0-0 libgtk-3-0 libxcomposite1 \
libxdamage1 libxrandr2 libxss1 libgbm1 libdrm2 libxkbcommon0 libcups2
See Integrations for what each tool is used for, and Windows Support for the Windows toolchain in detail.
Building
cargo build --release # optimized
cargo build # debug
cargo check # type-check only (fast)
./target/release/intendant setup browsers # managed Chrome for Testing for browser workspaces
Managed browser setup accepts --check, --force,
--channel stable|beta|dev|canary, --json, and --print-path; run
intendant setup browsers --help for exact usage.
A release build produces three binaries:
target/release/intendant-runtime— the command executor. Reads JSON commands on stdin, runs them, writes JSON results to stdout, and never holds API keys. Its write sandbox defaults on for macOS/Linux and is opt-in on Windows.target/release/intendant— the controller. Manages the LLM conversation, calls model APIs, dispatches tool calls to the runtime subprocess, and hosts every frontend (web dashboard, MCP, control socket).target/release/intendant-connect— the self-hostable account, discovery, and route-metadata rendezvous. It has no daemon authority.
The two-binary split is the security boundary; see Architecture.
Installing
cargo install --path .
All three binaries land in ~/.cargo/bin/. The intendant binary embeds the default
system prompts and the web assets (HTML, JS, compiled WASM) at compile time, so
it runs from any directory without the source tree.
WASM builds automatically
There are two browser WASM crates: crates/presence-web (fed by shared
crates/presence-core) produces static/wasm-web/, and
crates/station-web produces static/wasm-station/. A normal cargo build
rebuilds stale WASM for you: build.rs compares each crate’s source
timestamps with its compiled .wasm, invokes wasm-pack in a separate target
directory when needed, then re-embeds the resulting artifacts.
Artifact generation is version-gated. Only the wasm-pack version in
.wasm-pack-version — currently 0.14.0 — may rebuild the committed output.
If wasm-pack is absent or a different version is installed, cargo build
warns and keeps the committed artifacts rather than creating byte churn.
The canonical manual fallback rebuilds both crates with the same flags as
build.rs:
bash scripts/build-wasm.sh
cargo build --release -p intendant # re-embed
Regenerate and commit WASM artifacts on macOS only. The emitted bytes are not deterministic across host triples; the aarch64 macOS build is the canonical artifact host used by the drift gate.
The earlier guidance that “
cargo buildalone does NOT rebuild WASM” is no longer true —build.rshandles it.
macOS app bundle
scripts/bundle-macos.sh compiles a small Swift wrapper
(macos-app/*.swift) with swiftc, bundles it with the release intendant
binary, codesigns it (a persistent self-signed identity, ad-hoc fallback), and
installs to /Applications/Intendant.app. The same script carries the
release signing seam: with
INTENDANT_SIGN_IDENTITY and the notary variables set, it can produce a
Developer ID-signed, notarized bundle. Those credentials are not provisioned
for the current release setup, and no such artifact has been published.
The wrapper hosts a WKWebView that loads the dashboard over a custom
intendant:// URL scheme. This is deliberate: WKWebView does not treat
http://localhost as a secure context, so navigator.mediaDevices (microphone
and camera) would be unavailable. Serving from a registered custom scheme
restores the secure context the live-voice and camera features need.
When the generated access cert set is readable in
<state-root>/access-certs (~/.intendant/access-certs by default), the wrapper
automatically starts the bundled daemon
with native --mtls. The in-app intendant:// bridge then speaks HTTPS/WSS to
the local backend, pins the generated server certificate, and presents the
generated client.p12 for its own local bridge. Remote browsers use
https://<mac-ip>:<port> and must have an enrolled client identity. If no
complete access cert set is installed, the bundled daemon fails closed with setup
guidance rather than serving plain HTTP. Explicit launch flags still win:
open -a Intendant --args --tls forces TLS-only, and --no-tls --bind 127.0.0.1
is the explicit local plaintext/debug escape.
The same secure-context requirement applies to remote browsers using Station’s WebGPU renderer, microphone/camera features, browser screen capture, and other privileged browser APIs; see Web Dashboard: Secure Browser Contexts.
Dashboard on demand. The app starts the daemon immediately but defers the
dashboard SPA behind a lightweight placeholder with an Activate Dashboard
button — the SPA’s web-content process is the expensive part (it grows past a
gigabyte during long streaming sessions), and a Mac used as a remote daemon
never needs it locally. Closing the window frees the WKWebView entirely while
the daemon keeps running; reopen it from the Dock (the placeholder returns).
Quitting the app (Cmd+Q or the Dock menu) is what stops the daemon. An
activated dashboard whose window stays hidden or minimized for 3 hours unloads
back to the placeholder automatically. Two environment variables tune this:
INTENDANT_AUTO_DASHBOARD=1 loads the SPA at launch without the placeholder
(implied by INTENDANT_DIAG=1, which smoke harnesses rely on), and
INTENDANT_DASHBOARD_IDLE_UNLOAD_SECS overrides the idle-unload threshold
(0 disables it).
./scripts/bundle-macos.sh # release build + install to /Applications
./scripts/bundle-macos.sh debug # debug build + install
INSTALL_APP=0 ./scripts/bundle-macos.sh # build the bundle without installing
macOS releases: signing and notarization
The repository ships a tag-triggered release workflow with two independent
signing lanes. The PGP lane is required: pushing a v* tag runs
.github/workflows/release.yml on the self-hosted macOS runner, which
release-builds the binaries, runs scripts/bundle-macos.sh, detach-signs
every artifact it will publish with the Intendant release signing key
(scripts/release-pgp-sign.sh), publishes a GitHub Release — the app
archive Intendant-<version>-macos-<arch>*.zip, the stamped install.sh /
install.ps1, the .sha256 sidecars, a detached armored .asc signature
per artifact, and the public key itself — and commits the release manifest
(every artifact’s hash plus the signing-key fingerprint) to the public
transparency log. A tag build without the PGP secrets goes red before
building anything, and the sign step self-verifies every signature against
the repo-committed public key before anything publishes, so a release
whose own verify ritual would fail can never ship.
The Apple Developer ID lane is deliberately dormant for this alpha
(owner-ratified distribution lane, 2026-07-29): no Developer ID or
notarization credentials are provisioned and no signed/notarized app
release has been published, so the app archive keeps its explicit
-unsigned-dev suffix, Gatekeeper warns on first open, and the artifact
is not a distribution trust anchor — authenticity rides the PGP
signatures and the transparency log. The lane is parked, not deleted:
once its secrets are provisioned, a tag build engages it fully (signed
AND notarized, or the run goes red) on top of the PGP lane.
Verifying a release
The release signing key is committed at the repo root as
RELEASE-SIGNING-KEY.asc, published byte-identical beside every release,
linked from intendant.dev, and pinned at compile time by
intendant hosted-verify (src/bin/caller/pgp_identity.rs; a unit test
derives the pin from the committed bytes). Primary fingerprint (Ed25519,
certify-only primary + signing subkey):
A9B3 89C0 58DD 177B 3303 A135 22FC 08F0 A26D 3D18
# once — import the key (from a repo checkout, or the release's own assets)
gpg --import RELEASE-SIGNING-KEY.asc
gpg --fingerprint release@intendant.dev # compare against the fingerprint above
# per download — the signature proves the bytes, the log proves the history
gpg --verify Intendant-<version>-macos-arm64-unsigned-dev.zip.asc \
Intendant-<version>-macos-arm64-unsigned-dev.zip
intendant hosted-verify --releases <tag>
gpg --verify proves the artifact bytes were signed by the release key.
hosted-verify --releases proves the rest out of band: the release is
committed to the append-only transparency log under a tree head consistent
with the local pin, the logged signing-key fingerprint equals the binary’s
compiled-in pin, the published key asset hashes to exactly the committed
key bytes, every artifact carries its .asc, and GitHub’s asset metadata
matches the log (--download upgrades to re-hashing the artifacts;
shasum -a 256 -c <zip>.sha256 still works as a quick checksum).
Cryptographic signature verification deliberately stays gpg’s job — the
binary pins which key, gpg proves these bytes.
Installing a release: download the zip with its .asc and checksum
from GitHub releases,
verify as above, unzip, and drag Intendant.app to /Applications. On
first launch macOS asks for the TCC permissions (Screen Recording,
Accessibility, Microphone) exactly as with a local build — and, the alpha
being Apple-unsigned, Gatekeeper warns once on open. The app checks GitHub
for a newer release at launch (silently, release builds only) and via
Intendant → Check for Updates…; updating is always manual — the prompt
only opens the release page in your browser.
What the dormant Apple lane would add. With all signing and notarization
secrets provisioned, a release bundle is signed with a Developer ID Application certificate under the hardened runtime with a secure timestamp,
notarized by Apple, and stapled — Gatekeeper opens it without warnings, and a
tampered bundle fails validation. Signing is
inside-out: bundled non-system dylibs (Contents/Frameworks, e.g. libvpx),
then the embedded intendant-bin / intendant-runtime, then the bundle.
All executables carry the minimal entitlements in
macos-app/entitlements.plist — microphone (device.audio-input, live
voice), camera (device.camera, presence video input), and Apple Events
(automation.apple-events, the daemon’s osascript paths); the rationale for
what is present and absent is commented in that file. The app bundle stamps
its version (CFBundleShortVersionString) from the tag; local dev builds get
a git describe stamp, so a release archive and a local build are told apart
by version string, signatures, and the log — not by the shared
-unsigned-dev suffix. Versions are visible in Intendant → About
Intendant.
Provisioning the release secrets (repo → Settings → Secrets and
variables → Actions). Official tag builds fail closed on the PGP pair: a
v* tag must be PGP-signed and committed to the transparency log, or the
run goes red before anything is published. A workflow_dispatch dry-run
without secrets still exercises the pipeline end to end and keeps only the
explicitly -unsigned-dev artifact on the workflow run (with the PGP pair
configured, the dry-run’s workflow artifact is signed too). Each secret
group is all-or-nothing and a partial group fails the run.
PGP signing key — PGP_SIGN_KEY_B64, PGP_SIGN_KEY_PASSPHRASE
(required for tag releases):
- The release key lives in the operator escrow on the release owner’s
machine (
~/.intendant/release-signing/; its README documents the mint, rotation, and revocation ceremonies). The certify-only primary key never enters CI — only the signing subkey does, and it carries a two-year expiry as a dead-man switch. base64 -i ~/.intendant/release-signing/secret-subkey-ci.asc | pbcopy→PGP_SIGN_KEY_B64; the escrow’spassphrasefile content →PGP_SIGN_KEY_PASSPHRASE.- The release runner needs
gpgon PATH (brew install gnupgon the fleet Mac); the workflow imports the subkey into a throwawayGNUPGHOMEunderRUNNER_TEMPand removes it in analways()cleanup step. - Before cutting a release,
scripts/release-pgp-dryrun.shrehearses the whole lane locally — sign with the escrowed subkey, submit to a local rendezvous (including the log door’s refusal of an unsigned manifest),hosted-verify --releases --downloadagainst it, and the gpg ritual — with no tag, GitHub, or fleet involvement.
Signing identity — MACOS_SIGN_P12_B64, MACOS_SIGN_P12_PASSWORD
(dormant Apple lane):
- In your Apple Developer account (Certificates →
+), create a Developer ID Application certificate, or use Xcode → Settings → Accounts → Manage Certificates to create it in your login keychain. - In Keychain Access, expand the certificate, select the certificate
and its private key, File → Export Items… →
.p12, and set a strong password. Export with the certificate chain included (select the Developer ID intermediate alongside if offered) — the release job validates the identity and rejects chain-less exports. base64 -i cert.p12 | pbcopy, paste into theMACOS_SIGN_P12_B64secret; put the export password inMACOS_SIGN_P12_PASSWORD.
Notarization — NOTARY_KEY_B64, NOTARY_KEY_ID, NOTARY_ISSUER_ID:
- In App Store Connect → Users and
Access → Integrations → App Store Connect API → Team Keys, generate a
key with the Developer role. Download the
.p8(one-time download). base64 -i AuthKey_XXXX.p8 | pbcopy→NOTARY_KEY_B64; the key’s Key ID →NOTARY_KEY_ID; the page’s Issuer ID →NOTARY_ISSUER_ID.
The workflow imports the identity into a throwaway keychain (created, used,
and deleted with the original keychain search list restored even on failure)
and materializes the .p8 only for the run. To sign a build by hand instead,
scripts/bundle-macos.sh takes the same seam as environment variables:
INTENDANT_SIGN_IDENTITY (activates hardened-runtime distribution signing),
INTENDANT_SIGN_KEYCHAIN, INTENDANT_NOTARY_KEY_FILE /
INTENDANT_NOTARY_KEY_ID / INTENDANT_NOTARY_ISSUER (notarize + staple),
INTENDANT_ARTIFACT_DIR (versioned zip + checksum), and
INTENDANT_APP_VERSION (version override; defaults to git describe).
Native ChatGPT OAuth
Intendant Native can call OpenAI through a ChatGPT subscription without delegating the agent loop to Codex and without copying Codex’s auth file:
intendant auth chatgpt login # OpenAI device-code sign-in
intendant auth chatgpt status # source, masked account, and expiry
intendant auth chatgpt logout # best-effort remote revoke + local removal
intendant --provider openai --openai-auth chatgpt "Inspect this repository"
The login belongs to Intendant and lives at
<INTENDANT_HOME>/auth/openai-chatgpt.json (0600 on Unix and owner-private on
Windows). Intendant does not read or mutate ~/.codex/auth.json. The store is
atomically replaced under a cross-process lock when OpenAI rotates tokens; the
ID token is used only to derive the account identifier and is not persisted.
logout removes the local credential even if the best-effort provider revoke
fails. This is durable on-box subscription authority, so use a lease instead
when the box should go dry automatically.
--openai-auth (or OPENAI_AUTH_MODE) is deliberately separate from
--provider:
auto(default) preserves existing cost behavior: an OpenAI API key wins; otherwise an active native ChatGPT lease or local login is used.api-keyrequires meteredOPENAI_API_KEYauthority.chatgptrequires subscription OAuth authority and never falls back to an API key.
The API-key lane uses api.openai.com/v1/responses; the ChatGPT lane uses the
ChatGPT Codex Responses service with the account header, SSE wire shape,
encrypted reasoning continuity, and a stable session prompt-cache key. The
runtime subprocess never receives either credential.
The ChatGPT service accepts Intendant’s ordinary function tools but not the
metered Responses API’s native computer tool. Regular native sessions
therefore omit that one tool on the subscription transport instead of failing
the model turn. A dedicated OpenAI computer-use provider still requires
OPENAI_AUTH_MODE=api-key; automatic CU selection can fall through to a
configured Anthropic or Gemini provider.
API keys (.env)
Two ways to give a daemon credentials, by trust posture:
- Keys on disk (
.env) — this section. Right for a machine you fully trust: your laptop, a workstation you sit at. - No keys on disk — a daemon remains unfueled until an authorized trusted loopback/direct-mTLS session lends time-boxed leases from that daemon’s separate daemon-store vault, or relays supported provider calls through the trusted browser. A Connect account link or account-vault blob cannot fuel a daemon in this build because no delivery bridge ships; see Credential Custody. Right for rented, shared, or disposable boxes.
On startup the controller loads environment variables from .env, searching in
this order (later files do not override variables already set):
- Current directory and its parents (
dotenvy::dotenv()) - Project root — the git top-level,
<project-root>/.env - Global config —
~/.config/intendant/.env
.env and intendant.toml are git-ignored, so secrets never land in the repo.
For use-anywhere-after-cargo install, put your keys in
~/.config/intendant/.env:
# Provide at least one of these:
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GEMINI_API_KEY=AI...
# When more than one key is present, pick the main provider explicitly:
PROVIDER=openai # "openai" | "anthropic" | "gemini"
MODEL_NAME=gpt-5.5 # optional; a provider default is used if omitted
OPENAI, ANTHROPIC, and GEMINI are accepted as aliases for the
corresponding *_API_KEY variables. Provider auto-detection (when PROVIDER is
unset) prefers OpenAI when the selected OpenAI auth mode has authority
(API key first, then native ChatGPT OAuth in auto mode), then Anthropic, then
Gemini. See Configuration for the full environment
reference and per-provider default models.
Daemon installs run projectless. The search above runs once, at process startup, relative to wherever the daemon was launched — for the installed macOS app and service installs that is usually
$HOME. A launch directory without a project marker (.git/intendant.toml) is not treated as a project: the daemon runs projectless (its.envlayer for that directory comes only from the ordinary cwd walk-up, and each dashboard session picks its own project directory). The project you pick when creating a session does not feed this startup search. What a session’s project.envdoes contribute: the three provider key variables (and only those — neverPROVIDER, model, or endpoint settings) join that session’s key resolution as the last layer, after credential leases and the daemon’s own environment. For a daemon, the reliable places for keys are Dashboard → Settings → API Keys (persists to~/.config/intendant/.envand applies immediately, no restart), that global file itself (restart required if edited by hand), or vault leases per Credential Custody.
Your first run
# A one-off task. By default the web dashboard comes up (see below) and the
# controller prints the dashboard URL; open it in a browser to watch/steer.
./target/release/intendant "List the files in /tmp"
# Pipe a task in (non-TTY stdin auto-selects headless mode):
echo "summarize README.md" | ./target/release/intendant
# Interactive: with no task argument, you are prompted for one.
./target/release/intendant
Mode selection
The dashboard is the UI; the controller picks an execution shape from the flags:
- Web dashboard is on by default. It runs unless you pass
--no-web,--mcp, or--json. The server binds port 8765, auto-incrementing through 8785 if that port is taken; the chosen port is printed at startup. - Idle start (
intendantwith no task) runs the persistent daemon: the session supervisor owns every launch, driven from the dashboard. - A task on the command line runs as the foreground session under the same gateway, then falls through to the daemon loop when it ends.
--mcpturns the process into an MCP server on stdio (no dashboard).--jsonemits JSONL events to stdout (headless stdio; no dashboard).--no-webruns one headless task/session in the terminal (the agent loop may take many model/tool turns), with log output on stderr and no UI.--jsonadds scripted stdin follow-ups and approvals.
So a plain intendant "task" on a desktop gives you a dashboard URL; the
terminal itself is a launcher and log tail. (--no-tui from the retired
terminal UI era is still accepted as a no-op.)
Resume and continue
./target/release/intendant --continue "fix that bug" # most recent session
./target/release/intendant -c "fix that bug" # short form
./target/release/intendant --resume abc123 "continue" # by session id / prefix / path
./target/release/intendant -r abc123 "continue"
./target/release/intendant --resume # no id given → acts like --continue
Launching specific frontends
# Web dashboard explicitly (and/or pick a port)
./target/release/intendant --web
./target/release/intendant --web 9000
# Default dashboard: mTLS with installed access certs
./target/release/intendant
# TLS-only: HTTPS/WSS without requiring browser client certificates
./target/release/intendant --tls
# Explicit local/plaintext debug escape
./target/release/intendant --no-tls --bind 127.0.0.1
# Headless task/session (dashboard off)
./target/release/intendant --no-web "task"
# MCP server on stdio
./target/release/intendant --mcp "Deploy the application"
# Headless JSONL stream
./target/release/intendant --json "echo hello"
# macOS app bundle (after scripts/bundle-macos.sh)
open -a Intendant
# The bundle starts the backend with mTLS by default.
# Forward --tls only when you intentionally want TLS without client cert auth,
# or --no-tls --bind 127.0.0.1 only for explicit plaintext/debug use.
open -a Intendant --args --tls
CLI flag reference
The authoritative source is the argument parser in src/bin/caller/main.rs
(print_help and the parse loop). Flags that take a value error out if the
value is missing.
| Flag | Argument | Description |
|---|---|---|
--provider | <name> | Force provider: openai, anthropic, or gemini (sets PROVIDER) |
--openai-auth | <mode> | Select OpenAI authority/transport: auto, api-key, or chatgpt (sets OPENAI_AUTH_MODE; independent of --provider) |
--model | <name> | Override the model (sets MODEL_NAME) |
--task-file | <path> | Read the initial task from a file instead of argv |
--autonomy | <level> | Autonomy level: low, medium, high, full (loose parse; unknown → medium) |
--log-file | <dir> | Override the session log directory (default $INTENDANT_HOME/logs/<uuid>/, falling back to ~/.intendant/logs/<uuid>/) |
--continue, -c | — | Resume the most recent session for this project |
--resume, -r | [id] | Resume a session by id, prefix, or path; with no id behaves like --continue |
--no-tui | — | Deprecated no-op (the terminal TUI was removed); headless is --no-web |
--mcp | — | Run as an MCP server on stdio (disables the dashboard) |
--verbose, -v | — | Show debug-level log entries |
--control-socket | — | Enable the Unix control socket at /tmp/intendant-<pid>.sock |
--json | — | Emit JSONL events to stdout (headless stdio; disables the dashboard) |
--sandbox | — | Force filesystem sandboxing on for the runtime (Landlock on Linux 5.13+, Seatbelt on macOS, restricted tokens on Windows). Wins even if --no-sandbox is also present |
--no-sandbox | — | Force the runtime write sandbox off. With neither flag, [sandbox] enabled wins when set; otherwise the platform default is on for macOS/Linux and off for Windows |
--direct | — | Force single-agent mode (skip the orchestrator / sub-agent delegation) |
--no-presence | — | Disable the presence layer (talk to the worker agent directly) |
--web | [port] | Start the web dashboard. On by default; optional numeric port (default 8765) |
--no-web | — | Disable the web dashboard; run headless |
--bind | <addr> | IP address for the web dashboard listener. Use 127.0.0.1 for local/plaintext automation |
--no-tls | — | Serve the dashboard over plain HTTP. Explicit local/debug escape; wildcard bind refuses startup when a public interface exists |
--allow-public-plaintext | — | Override the --no-tls public-interface startup guard; does not grant remote callers local/root authority |
--tls | — | Serve HTTPS/WSS and provide a secure context; certless local-root is loopback-only and remote protected routes still require mTLS |
--tls-cert | <path> | PEM cert (chain) overriding the default cert selection; implies --tls (pair with --tls-key) |
--tls-key | <path> | PEM private key matching --tls-cert; implies --tls |
--mtls | — | Require browser/client certificates signed by the Intendant access CA. This is the default dashboard transport |
--mtls-ca | <path> | PEM CA bundle for --mtls client certificate verification |
--transcription | — | Enable server-side speech transcription (overrides [transcription] enabled) |
--record-display | <id> | Record an existing X11 display, e.g. 50 for :50 (repeatable) |
--agent | <backend> | Use an external coding-agent backend: codex, claude-code, kimi, or pi |
--advertise-url | <url> | WebSocket URL to advertise to federation peers in this daemon’s Agent Card (repeatable, preference order; overrides [server.advertise]) |
--help, -h | — | Print help and exit |
--version, -V | — | Print version + build provenance (package version, commit short SHA with -dirty marker, build timestamp, target triple) and exit. Also accepted by intendant-runtime; the daemon reports the same line as daemon_version in intendant ctl status |
Correction vs. older docs:
--webis on by default and no longer “implies--mcp”. The dashboard runs unless disabled by--no-web,--mcp, or--json. Earlier documentation treated--webas opt-in — that is no longer accurate.
A non-flag token (one that does not start with -) is collected into the task
string; an unknown flag is an error.
Native provider authentication is a keyless administrative subcommand:
intendant auth chatgpt login|status|logout. It is intercepted before project
and provider initialization, so status, login, and logout work even when the
daemon has no current model authority.
Dashboard access over TLS
The dashboard defaults to native mTLS. Run intendant access setup first so the
daemon has server.crt, server.key, and ca.crt, and so dashboard consumer
devices can enroll a client identity.
1. Built-in mTLS / TLS (any platform, pure-Rust)
./target/release/intendant # default: mTLS
./target/release/intendant --tls # TLS-only
With no transport flag, the gateway serves HTTPS/WSS with client-certificate
authentication required. --tls (or [server.tls] enabled = true in
intendant.toml) serves HTTPS/WSS without requiring browser client
certificates. With no explicit cert override, TLS-only first uses the installed
Intendant access server certificate (server.crt / server.key) when present,
then falls back to minting a self-signed certificate at startup (SAN = bind IP +
localhost, plus an optional configured hostname). The TLS stack is pure Rust
(rustls + rcgen) — no OpenSSL, no nginx — and works on Windows too. See
Configuration for [server.tls] and --tls-cert /
--tls-key.
intendant access setup stores the access CA, server cert, and client identity in
the current user’s native cert store (<state-root>/access-certs on macOS/Linux,
the OS data directory on Windows). Run setup as the same user that launches the daemon; the native gateway
reads server.crt / server.key directly from that store.
To be explicit about the default, pass --mtls:
./target/release/intendant --mtls
--mtls verifies browser/client certificates against the installed Intendant
access CA (ca.crt) unless --mtls-ca or [server.mtls] ca overrides it.
Clients without the installed client identity cannot complete the TLS handshake.
If access certs are missing, startup fails closed with setup guidance. Use
--no-tls only when you intentionally want plain HTTP for local/programmatic
debugging. Prefer --no-tls --bind 127.0.0.1; wildcard plaintext refuses
startup when the host has a public interface unless --allow-public-plaintext
is passed.
2. Native access cert enrollment (intendant access setup)
For mutual-TLS with client certificates (so only enrolled devices can connect),
the intendant access subcommand creates a per-user access certificate authority,
server cert, client identity, and local-IAM owner grant. macOS/Linux setup can
then start a strict enrollment page for other browsers and mobile devices;
Windows setup instead prints exact PowerShell commands for importing the same
material into that user’s local certificate stores:
intendant access setup # generate and seed CA/server/client material
intendant access recert # regenerate the server cert after access addresses change
intendant access list # show current setup state
intendant access serve-certs # macOS/Linux: strict HTTPS client enrollment
intendant access remove # remove the per-user access cert store
Useful flags: --port <N> (native dashboard HTTPS port to advertise, default
8765),
--cert-port <N> (HTTPS enrollment server, default 9999), repeatable
--ip <IP>, repeatable --host <DNS>, --name <label>, --force, and
--no-serve-certs. --cert-port and --no-serve-certs are accepted but unused
on Windows, where setup never starts a distribution server. Removed LAN/proxy
flags are rejected; certificate setup no longer configures an upstream proxy.
--name is the daemon display label used by the Agent Card and dashboard
targets. If omitted, setup now uses the system hostname when available and falls
back to the primary IP only as a last resort. Existing IP-based labels continue
to work, but the dashboard prefers a human label or hostname over a stale IP
when both are present.
On Windows, setup finishes after generation and prints copy/paste-ready
Import-Certificate / Import-PfxCertificate commands targeting
Cert:\CurrentUser\Root and Cert:\CurrentUser\My. The commands read the
generated PKCS#12 password directly into a PowerShell SecureString; they do
not echo it into shell history. Close and reopen Edge or Chrome afterwards.
serve-certs returns an explicit unsupported/manual-import error on Windows.
remove deletes the generated access directory, but certificates manually
imported into Windows certificate stores remain until removed with
certmgr.msc.
Remote client certificate enrollment is deliberately strict. The temporary enrollment
server is HTTPS, using the same access server certificate as the dashboard. Before
the CLI reveals the one-time enrollment secret, the operator must copy the
server certificate SHA-256 fingerprint observed in the browser certificate UI
and paste it into the intendant access terminal. Treat any enrollment web content
as untrusted until that terminal check succeeds: stop at the browser certificate
warning/details UI, do not continue to page content, enter secrets, click
downloads, or install anything first. If the browser cannot expose certificate
details before loading the page, inspect the server certificate with a
client-side certificate tool instead. Only after the pasted fingerprint matches
the local server.crt can the browser redeem the secret and download enrollment
artifacts. Apple clients get a single intendant.mobileconfig profile; other
clients can download ca.crt plus client.p12 (or the same identity as
client.pfx). This avoids the unsafe pattern where a MITM page can serve
active content or steal a secret from an unauthenticated download page. A fleet
DNS/WebPKI certificate is deliberately not an enrollment shortcut: the server
always presents the direct access certificate and always requires the
browser-observed fingerprint. Hosted DNS or origin control must never authorize
release of the shared owner/root client bundle.
The enrollment page uses the browser’s User-Agent only to prioritize the instructions and download buttons for that device. The authorization decision is always the strict terminal-paired session cookie.
Use this HTTPS/mTLS path or native --tls with a trusted certificate for
dashboard features that require a secure browser context. A locally built
macOS app wrapper also supplies a secure context for development, but is not a
distribution anchor:
Station WebGPU, microphone/camera, browser screen capture, and stricter
clipboard APIs. Plain http://<host-ip>:8765 is available only when launched
with --no-tls; it is intended for local/programmatic debugging, is guarded on
public wildcard binds, and does not enable those browser-gated features.
The client certificate is exported as client.p12, a password-protected
PKCS#12 bundle for installation on iOS / Android / desktop browsers. The
password is shown only on the unlocked enrollment page. The Apple
intendant.mobileconfig profile embeds that same client identity, the access CA,
and the PKCS#12 password, so it is served only after strict pairing succeeds.
On macOS, install downloaded profiles from System Settings → General → Device
Management; if that pane is hidden, search System Settings for “Profiles”.
If Safari still shows Not Secure, open Keychain Access and set the
Intendant CA to Always Trust; installing the profile is not enough unless
the CA is trusted for websites.
If macOS reports that the profile certificate could not be verified, install
ca.crt and client.p12 manually from the same unlocked page, or regenerate
access cert material with intendant access setup --force so the Apple profile
uses an Apple-compatible client identity bundle and certificate payloads.
Apple device requirement for client.p12
client.p12 is packaged in Apple auto-detect-compatible PKCS#12 form: PBES1
3DES-CBC with a SHA-1 MAC. This is intentionally less modern than PBES2/AES
because macOS profile installation and security import can reject PBES2/AES
bundles as a password/MAC authentication error unless the caller explicitly
forces PKCS#12 format. New access cert material still uses RSA-2048 certificates
with SHA-256 signatures, matching Apple’s documented certificate
configuration-profile payload compatibility.
Android and desktop Chrome/Firefox also import the Apple-compatible bundle.
Testing
cargo test -p intendant --bins
cargo test -p intendant-core -p intendant-display -p intendant-platform \
-p owner-plane-core -p owner-plane-reducer
cargo test -p intendant --test e2e
cargo clippy --workspace -- -D warnings
These keyless commands cover all three package binaries, the workspace library
gates (including the stamped owner-plane corpus), the real-binary E2E suite,
and workspace-wide Clippy. Unit tests are inline #[cfg(test)] modules. The
#[tokio::test] cases in tests/e2e/main.rs spawn the real binaries against
the deterministic scripted mock provider (PROVIDER=mock +
INTENDANT_MOCK_SCRIPT). They need no API key or network, use the synthetic
1280×720 display backend rather than native capture, and run in CI on macOS,
Linux, and Windows. Real-LLM or headed-display scenarios live under
tests/skills/ and are deliberately outside CI. See
Session Logging for the logging/search coverage
exercised through that real-binary suite.
Runtime (standalone)
The runtime executes a JSON command batch on stdin and writes results to stdout:
echo '{"commands":[{"function":"execAsAgent","nonce":1,"command":"echo hello"}]}' \
| ./target/release/intendant-runtime
See Runtime Protocol for the full command schema.