reference
deputyctl
Single management surface for an deputyOS appliance: lifecycle, profile switching, model providers, doctor / limits, updates, backup, restore, cost guardrails, and quiet hours.
Single management surface for an deputyOS appliance: lifecycle, profile switching, model providers, doctor / limits, updates, backup, restore, cost guardrails, and quiet hours.
deputyctl is the operator-facing binary of deputyOS. On a baked image
it lives at /usr/local/bin/deputyctl and is invoked directly (it is not
a systemd service — long-running daemons are the gateway services and
deputypwa, not deputyctl). Every subcommand is sync I/O
end-to-end; there is no tokio runtime in this binary, which keeps it small
enough to ship into the smallest baked targets.
The command surface is the frozen command surface from
docs/02-profiles.md
plus two intentionally additive surface extensions (cost, quiet-hours)
added in a later release.
Where to look first
- Operational quick-start: operations/monitoring-and-logs.md.
- Profile concepts: concepts/profile-class.md.
- Filesystem layout it reads/writes: reference/system/filesystem-layout.md.
Synopsis
deputyctl [-v|-vv] <command> [command options]
Global options
| Flag | Effect |
|---|---|
-v, --verbose | Increase log level. -v = debug, -vv = trace. Counted; passing -vvv clamps at trace. Overrides the default info and is itself overridden by RUST_LOG if RUST_LOG is set. |
-h, --help | Print top-level or per-subcommand help. Each deputyctl <cmd> --help page is autogenerated by clap from the same struct that drives dispatch. |
-V, --version | Print the deputyctl crate version (clap-default; the richer info comes from deputyctl version). |
Logging
deputyctl uses tracing with tracing-subscriber
configured to write to stderr (never stdout — stdout is reserved for
machine-readable output such as --json). The default filter is info. Set
RUST_LOG to override:
RUST_LOG=deputyctl=debug,info deputyctl doctor
The verbose flag (-v, -vv) sets the default but loses to an explicit
RUST_LOG. Time and target are stripped from log lines so they remain
readable in journal output.
Environment variables
deputyctl resolves every on-disk path through deputyctl/src/paths.rs.
Each path has an environment override so the smoke harness and contributor
laptops can stage fixtures without root.
| Variable | Default | Purpose |
|---|---|---|
DEPUTYOS_PROFILES_DIR | /etc/deputyos/profiles (else ./profiles) | Directory of *.toml profile manifests. |
DEPUTYOS_ACTIVE_PROFILE_FILE | /etc/deputyos/active-profile | One-line file naming the active profile id. Atomically rewritten by profile switch. |
DEPUTYOS_LIMITS_FILE | /etc/deputyos/limits.json (else deputyctl/etc/limits.qemu-aarch64.json) | Per-device capability + limitation report. Read by limits and the update target picker. |
DEPUTYOS_PROVIDERS_FILE | /etc/deputyos/providers.json (else deputyctl/etc/providers.json) | Provider catalogue. Read by model list, model set, model test. |
DEPUTYOS_SECRETS_FILE | /etc/deputyos/secrets.env | KEY=VALUE env file holding active provider credentials. Mode 0600. |
DEPUTYOS_ACTIVE_PROVIDER_FILE | /etc/deputyos/active-provider | One-line file naming the configured provider id. |
DEPUTYOS_RCLONE_CONFIG | /etc/deputyos/rclone.conf | rclone config for backup / restore. |
DEPUTYOS_BACKUP_CONFIG | /etc/deputyos/backup.toml | Backup destination + retention. |
DEPUTYOS_HOOKS_DIR | /etc/deputyos/hooks.d | Root of pre-message/, post-message/, cost-alert/, update-applied/ hook script subdirs. |
DEPUTYOS_SYSTEMD_UNIT_DIR | /etc/systemd/system | Where backup schedule writes the timer + service. |
DEPUTYOS_WIZARD_STATE_FILE | /var/lib/deputyos/wizard-state.json | Cleared by factory-reset. |
DEPUTYOS_DEV_OUT | unset | When set, all invasive writes (factory-reset, schedule, cost) are redirected under this dir; systemctl daemon-reload is skipped. The convention used by smoke tests on a contributor laptop. |
DEPUTYOS_COST_CONFIG | /etc/deputyos/cost.toml | Cost + quiet-hours config. |
DEPUTYOS_COST_LEDGER | ~/.<active_profile>/cost-ledger.jsonl | Append-only JSONL ledger written by the agent profile. |
DEPUTYOS_COST_TRIPPED | /var/lib/deputyos/cost-tripped | Marker that gates the active profile when caps trip. |
DEPUTYOS_COST_DEFAULTS | /etc/deputyos/cost-defaults.json (else deputyctl/etc/cost-defaults.json) | Provider rate sheet for fallback cost estimation. |
DEPUTYOS_UPDATE_URL | content of /etc/deputyos/update-url, else file://./dist/manifest.json | Manifest URL for update --check. |
DEPUTYOS_UPDATE_PUBKEY | /etc/deputyos/pubkey.minisign, else ./dist/pubkey.minisign, else ~/.config/deputyos/dev-keys/deputyos-dev.pub | Public key that signs the manifest. |
DEPUTYOS_INSTALLED_VERSION | content of /etc/deputyos/version | Reported by version and consulted by update --check. |
DEPUTYOS_TARGET | content of limits.json’s target field | Hardware target (e.g. qemu-aarch64). |
DEPUTYOS_ACTIVE_PROFILE | content of DEPUTYOS_ACTIVE_PROFILE_FILE | Override for the update artefact picker. |
DEPUTYOS_STAGING_DIR | /var/lib/deputyos/staging | Where update --apply stages downloaded artefacts. |
DEPUTYOS_SLOTS_FILE | /var/lib/deputyos/slots.json | A/B slot record consulted by rollback. |
DEPUTYOS_RELAY_SOCKET | /run/deputyos/relay.sock | Default socket path for the hidden --internal-run-relay flag. |
DEPUTYWIZARD_TOKEN_FILE | /run/deputyos/wizard.token | Used by deputyctl init indirectly via the spawned deputywizard. |
DEPUTYOS_CLOUDFLARED | cloudflared | Override the cloudflared binary used by tunnel. |
DEPUTYOS_CLOUDFLARED_PID | /run/deputyos/cloudflared.pid | PID file written by tunnel --background. |
Exit codes
deputyctl follows BSD sysexits.h conventions where they make sense and falls
back to traditional 0 / 1 everywhere else.
| Code | Meaning |
|---|---|
0 | Success. For doctor / status this also implies “no failing checks”. |
1 | Runtime error. Network, signature, missing file, dependent process exited non-zero. The user-facing error is printed to stderr prefixed deputyctl: error:. |
64 | Usage error (EX_USAGE). Unknown subcommand, conflicting flags (e.g. both --check and --apply for update), missing required interactive input under non-TTY stdin, empty snapshot id. Scripts can probe this code to distinguish “you told me wrong” from “the world is broken”. |
Individual subcommands document additional codes where they apply.
Commands
init
Run the first-boot wizard.
Synopsis
deputyctl init
Options
(none)
Behavior
- Locate the deputywizard binary. Probes
PATHfirst; then falls back totarget/debug/deputywizardandtarget/release/deputywizardrelative to the current directory socargo run -p deputyctl -- initworks in a dev tree. - Print the URL the user should open:
http://localhost:8088/wizard. - Spawn
deputywizard serve --port 8088and wait for it to exit.
This is the path used by support flows (“re-run the wizard after factory
reset”). On a baked image, the first boot runs deputywizard.service
directly via systemd (gated on ConditionPathExists=!/var/lib/deputyos/wizard-state.json);
deputyctl init is the manual re-run path.
Exit codes
0— wizard exited cleanly.1— deputywizard binary not found, or wizard exited non-zero.
Related
profile list
Show installed profiles + versions.
Synopsis
deputyctl profile list [--json]
Options
--json— emit a JSON array ofInstalledProfileobjects.
Behavior
Enumerates *.toml files under DEPUTYOS_PROFILES_DIR, parses each manifest
through deputyctl/src/manifest.rs, and marks the active one (matched by id
against DEPUTYOS_ACTIVE_PROFILE_FILE’s contents) with a leading *.
A malformed manifest in the dir is a hard error — the image-rev contract
requires every shipped manifest to parse — so profile list exits 1 if any
file fails to load.
Examples
$ deputyctl profile list
* openclaw OpenClaw 2026.4.27 stable
hermes Hermes 0.12.0 stable
khoj Khoj 2.1.0 beta
$ deputyctl profile list --json
[
{
"id": "openclaw",
"display_name": "OpenClaw",
"pinned_version": "2026.4.27",
"release_channel": "stable",
"manifest_path": "/etc/deputyos/profiles/openclaw.toml",
"active": true
},
...
]
Installed profile shape
{
"id": "string", // kebab-case, e.g. "openclaw"
"display_name": "string",
"pinned_version": "string", // YYYY.M.D or semver
"release_channel": "string", // "stable" | "beta" | ...
"manifest_path": "string", // absolute path to the .toml
"active": "bool"
}
Files read
DEPUTYOS_PROFILES_DIR/*.tomlDEPUTYOS_ACTIVE_PROFILE_FILE
Related
profile status
Detail on the active profile.
Synopsis
deputyctl profile status [--json]
Options
--json— JSON output.
Behavior
- Read the active profile id from
DEPUTYOS_ACTIVE_PROFILE_FILE. - Load that profile’s manifest.
- Probe the systemd unit named in
[service].unitviasystemctl is-active. - Print id, display name, pinned version, install root, data dir, unit, and active state.
If no active profile exists (file missing or empty), prints no active profile
to stderr and exits 1.
Examples
$ deputyctl profile status
id: openclaw
display_name: OpenClaw
pinned_version: 2026.4.27
install_root: /opt/deputyos/profiles/openclaw
data_dir: /home/agent/.openclaw
unit: openclaw-gateway.service
active_state: active
{
"id": "openclaw",
"display_name": "OpenClaw",
"pinned_version": "2026.4.27",
"install_root": "/opt/deputyos/profiles/openclaw",
"data_dir": "/home/agent/.openclaw",
"unit": "openclaw-gateway.service",
"active_state": "active"
}
Files read
DEPUTYOS_ACTIVE_PROFILE_FILE- The active profile manifest under
DEPUTYOS_PROFILES_DIR.
Related
profile switch <id>
Atomic active-profile swap.
Synopsis
deputyctl profile switch <id> [--yes] [--dry-run]
Options
<id>— profile id (e.g.openclaw,hermes,khoj). Must already be installed.-y, --yes— skip the interactive confirmation prompt. Required for non-TTY usage.--dry-run— print the planned actions and exit 0 without changes.
Behavior
The switch is implemented in
deputyctl/src/profile_switch.rs.
Per docs/02-profiles.md:
- Validate the target profile id exists in
DEPUTYOS_PROFILES_DIR. - If already active, print
already active: <id>and exit 0. - If not
--yesand not--dry-run, prompt[y/N]on the TTY. Refusing or non-TTY input aborts with exit 1. - Print the plan.
- Stop the current profile’s systemd unit (best effort).
- Atomic swap of the active-profile pointer: write
<active-profile-file>.tmpthenrename(2)over the real path. - Reload AppArmor profile (best effort, via
apparmor_parser -r). - Start the new profile’s systemd unit (best effort).
- Run every entry in
[upgrade].post_upgrade_hooksthroughsh -c. - Print final unit state.
The swap is committed the moment step 6 completes — any subsequent
failure is logged as warn: but does not unwind. Recovery from a bad
post-hook is the user’s job (deputyctl doctor to identify, deputyctl profile switch <old> to revert).
Disruption
profile switchstops the current agent and starts a new one. In-flight chats on the old profile are dropped. There is no transparent migration of conversation state across profiles.
Examples
$ deputyctl profile switch hermes
plan:
- stop unit for current profile "openclaw"
- swap /etc/deputyos/active-profile -> "hermes"
- reload AppArmor profile deputyos.hermes
- start unit hermes-gateway.service
- run post-upgrade hook: /opt/deputyos/profiles/hermes/post-upgrade.sh
switch profile "openclaw" -> "hermes"? [y/N] y
switched: openclaw -> hermes
unit: hermes-gateway.service
state: active
$ deputyctl profile switch khoj --dry-run
plan:
- stop unit for current profile "openclaw"
- swap /etc/deputyos/active-profile -> "khoj"
- reload AppArmor profile deputyos.khoj
- start unit khoj-gateway.service
(dry-run) no changes made
Exit codes
0— switch completed (or already active, or dry-run).1— unknown id, user aborted, or non-TTY without--yes.
Files read / written
- Read: target manifest under
DEPUTYOS_PROFILES_DIR/<id>.toml. - Written:
DEPUTYOS_ACTIVE_PROFILE_FILE(atomic rename). - Side effects:
systemctl stop <old>,systemctl start <new>,apparmor_parser -r.
Related
profile validate <paths>...
Validate one or more profile manifests against the schema + invariants.
Synopsis
deputyctl profile validate <paths>... [--json]
Options
<paths>...— one or more.tomlpaths. Globs are expanded by the shell.--json— emit a structured JSON report.
Behavior
For each path, runs the validator from
deputyctl/src/validate.rs. Validation does not short-circuit on the first
error; every violation in every file is reported in one pass — so a CI run
surfaces all problems at once. Rules:
profile.idmust match^[a-z][a-z0-9-]*$and equal the file stem.paths.install_rootmust be absolute and start with/opt/deputyos/profiles/<id>.paths.data_dir,paths.binarymust be absolute.service.unitmust end with.service.service.entrypointmust be non-empty.runtime.language∈ {node,python,rust,go, …};package_managerconsistent.health.http_checkmust be a URL.health.startup_grace_s> 0.apparmor.profile(if set) must be a valid identifier.[[channels]]entries must each declareid,display_name,kind.
Examples
$ deputyctl profile validate profiles/openclaw.toml profiles/hermes.toml
validated: 2/2
$ deputyctl profile validate /tmp/bad.toml
/tmp/bad.toml: profile.id: "Bad-ID" must match ^[a-z][a-z0-9-]*$ ...
/tmp/bad.toml: paths.install_root: must start with "/opt/deputyos/profiles/bad-id" ...
validated: 0/1
{
"results": [
{
"path": "profiles/openclaw.toml",
"ok": true,
"errors": []
}
]
}
Exit codes
0— every path validated cleanly.1— at least one file failed validation.
Related
up, down, restart
Lifecycle controls for the active profile’s systemd unit.
Synopsis
deputyctl up
deputyctl down
deputyctl restart
Behavior
- Verify systemd is available (
systemd::available()checks for/run/systemd/system). - Load the active profile’s manifest.
- Run
systemctl <verb> <unit>where<verb>∈ {start,stop,restart} and<unit>is the manifest’s[service].unit.
Exit codes
0— systemctl succeeded.1— systemd unavailable, no active profile, or systemctl exited non-zero.
Related
status
Short health summary of the active profile.
Synopsis
deputyctl status
Behavior
Reads the active profile, queries systemctl is-active, runs the
“quick fail count” subset of doctor (no network, fast), and prints:
profile: <id>
unit: <unit>
state: <active|inactive|failed|...>
doctor: <N> failing check(s)
then prints the systemctl status <unit> block (truncated).
Exit codes
0— unit isactive.1— unit not active, systemd unavailable, or no active profile.
Related
logs
Stream the active profile’s journal.
Synopsis
deputyctl logs [-f|--follow]
Options
-f, --follow— pass-fto journalctl (live tail).
Behavior
journalctl -u <health.journal_unit> (which usually equals service.unit,
but can be overridden in the manifest if a profile splits its logging
across multiple units).
Exit codes
0— journalctl exited normally.1— systemd unavailable, no active profile, or journalctl exited non-zero.
shell
Drop into the profile’s CLI.
Synopsis
deputyctl shell [--dry-run]
Options
--dry-run— print the plannedexecand exit without running.
Behavior
Loads the active profile manifest and exec(2)s the binary at
paths.binary, optionally with a per-profile sub-arg:
| Profile | Default sub-arg |
|---|---|
openclaw | repl |
hermes | shell |
| (other) | (none) |
Because exec replaces the process image, ps and top show the agent
binary directly — no deputyctl parent. If the binary is not present (typical
on a contributor laptop), exits 64 with a message that this requires a
baked image.
Exit codes
0—--dry-runsucceeded.1—execitself failed (extremely rare).64— no active profile or binary missing.
doctor
Full health check; nonzero on any failure.
Synopsis
deputyctl doctor [--json]
Options
--json— emit aDoctorReportinstead of the table.
Behavior
Runs the ordered list of checks defined in
deputyctl/src/doctor.rs::all_checks():
| Check | Expects |
|---|---|
active-profile-manifest | active-profile points at a parseable manifest |
apparmor-enforcing | AppArmor loaded with deputyos.<profile> in enforce |
ufw-default-deny | ufw status reports default deny incoming + ufw active |
fail2ban-running | fail2ban service is active |
clamav-healthy | clamd active OR (per limits.json) RAM-tier disables clamd |
magika-present | magika binary on PATH |
ssh-key-only | sshd config: PasswordAuthentication no, PermitRootLogin no |
hardened-sysctls | the deputyOS sysctl set is applied |
secrets-mode-0600 | /etc/deputyos/secrets.env mode is exactly 0600 |
agent-user-exists | a user named agent is present |
zram-active | /dev/zram0 exists when limits.json requires it |
| … | (full list in source — extending is vec![Check {...}, ...]) |
Each check returns one of Pass, Warn(detail), Fail(detail),
Skip(detail). Skip is used on non-Linux dev hosts so cargo test works
on macOS — the production answer is “must be Linux”.
Examples
$ deputyctl doctor
PASS active-profile-manifest
PASS apparmor-enforcing
FAIL ufw-default-deny ufw is inactive
fix: sudo ufw default deny incoming && sudo ufw enable
PASS fail2ban-running
SKIP clamav-healthy skipped on tier "lite" (clamd disabled by RAM)
...
3 fail / 1 warn / 18 pass / 2 skip
Doctor JSON shape
{
"linux": true,
"checks": [
{
"name": "active-profile-manifest",
"outcome": { "kind": "Pass" },
"fix": "ensure /etc/deputyos/active-profile names an installed profile"
},
{
"name": "ufw-default-deny",
"outcome": { "kind": "Fail", "detail": "ufw is inactive" },
"fix": "sudo ufw default deny incoming && sudo ufw enable"
}
],
"fails": 1,
"warns": 0,
"passes": 1,
"skips": 0
}
Exit codes
0—fails == 0(warns are tolerated bydoctoritself;make cimay treat them differently).1— at least one Fail.
Related
limits
Per-device capability + limitation report.
Synopsis
deputyctl limits [--json]
Options
--json— emit the rawLimitsstruct.
Behavior
Loads DEPUTYOS_LIMITS_FILE (defaults to /etc/deputyos/limits.json, which is
baked at image build time by the Lane B Ansible role) and renders it human-
readable per docs/14-limitations.md:
Device: rpi5-8gb (8192 MB RAM, sd-card storage)
Tier: standard
What this device CAN do:
✓ Run a local LLM
✓ Voice wake-word
✓ On-demand virus scan via Magika + clamscan
✓ Heavy channels available: voice, vision
What this device CANNOT do (and why):
✗ background-large-models (RAM tier insufficient for >7B param models persistently)
Unblock: switch to rpi5-16gb or x86 mini-PC
Channels disabled by RAM tier: video-call
Currently active warnings:
(none)
Run `deputyctl doctor` for live pressure metrics.
Limits JSON shape
{
"target": "rpi5-8gb",
"tier": "standard",
"ram_mb": 8192,
"ram_class": "8GB",
"storage_class": "sd-card",
"capabilities": {
"local_llm": true,
"voice_wake_word": true,
"voice_tts": true,
"clamav_daemon": false,
"channels_heavy": ["voice", "vision"],
"channels_disabled_by_ram": ["video-call"]
},
"limitations": [
{
"id": "background-large-models",
"reason": "RAM tier insufficient for >7B param models persistently",
"unblock": "switch to rpi5-16gb or x86 mini-PC"
}
]
}
Files read
DEPUTYOS_LIMITS_FILE.
Related
version
Print SBOM + build manifest summary.
Synopsis
deputyctl version [--json]
Options
--json— emit JSON instead of key-value text.
Behavior
Reports:
binary_version— the deputyctl crateCARGO_PKG_VERSION.git_sha— from compile-time envDEPUTYOS_GIT_SHA(build script sets this;unknownin dev).build_date— from compile-time envDEPUTYOS_BUILD_DATE.target— host triple fromTARGETenv at build time.kernel—/proc/sys/kernel/osrelease.apparmor— count of profiles loaded into/sys/kernel/security/apparmor/profiles.profiles_dir— resolved path.profiles— array of installed profile summaries with active marker.
Examples
$ deputyctl version
deputyctl version: 0.1.0
git_sha: a1b2c3d
build_date: 2026-04-26
target: aarch64-unknown-linux-gnu
kernel: 6.6.28+rpt-rpi-2712
apparmor: loaded (47 profiles)
profiles_dir: /etc/deputyos/profiles
profiles:
- openclaw (OpenClaw @ 2026.4.27) *active*
- hermes (Hermes @ 0.12.0)
- khoj (Khoj @ 2.1.0)
{
"binary_version": "0.1.0",
"git_sha": "a1b2c3d",
"build_date": "2026-04-26",
"target": "aarch64-unknown-linux-gnu",
"kernel": "6.6.28+rpt-rpi-2712",
"apparmor": "loaded (47 profiles)",
"profiles_dir": "/etc/deputyos/profiles",
"profiles": [ ... ]
}
Exit codes
0— always.
model list
Show configured + available providers.
Synopsis
deputyctl model list [--json]
Options
--json— emit a structured JSON list.
Behavior
Loads DEPUTYOS_PROVIDERS_FILE (the bake-time provider catalogue), checks
each provider’s key_env_var against DEPUTYOS_SECRETS_FILE, and prints the
table. configured is true iff a non-empty value for that env var exists in
secrets.env.
Examples
$ deputyctl model list
id display_name key_env_var status
openrouter OpenRouter OPENROUTER_API_KEY configured
openai OpenAI OPENAI_API_KEY not configured
anthropic Anthropic ANTHROPIC_API_KEY not configured
...
[
{
"id": "openrouter",
"display_name": "OpenRouter",
"kind": "openai-compatible",
"configured": true,
"key_env_var": "OPENROUTER_API_KEY",
"default_model": "anthropic/claude-3.5-sonnet"
}
]
Files read
DEPUTYOS_PROVIDERS_FILEDEPUTYOS_SECRETS_FILE
Related
model set
Interactive provider/model picker.
Synopsis
deputyctl model set [--provider <id>] [--key-from-stdin] [-y|--yes]
Options
--provider <id>— skip the picker prompt; jump straight to entering a key.--key-from-stdin— read the key from stdin instead of prompting on TTY (required for piping).-y, --yes— skip the confirmation prompt.
Behavior
- If
--provideris unset and stdin is a TTY: print the catalogue, prompt for a number. Empty input or invalid number aborts. - If
--provideris unset and stdin is not a TTY: exit64(you must pass--provider). - Resolve the key:
--key-from-stdin→ read one line from stdin.- TTY → masked prompt via
rpassword. - non-TTY without
--key-from-stdin→ exit64.
- Empty key → exit
1. - Confirm
[y/N]unless--yes. - Atomic write of the key into
DEPUTYOS_SECRETS_FILE. Existing keys for other providers are preserved; the line for the target env var is replaced in place if present, appended if absent. The temp file is created with mode0600before the rename. - Atomic write of the provider id into
DEPUTYOS_ACTIVE_PROVIDER_FILE.
Examples
$ deputyctl model set
available providers:
1. openrouter OpenRouter (OPENROUTER_API_KEY)
2. openai OpenAI (OPENAI_API_KEY)
...
pick a number (or blank to abort): 1
enter key for OpenRouter (OPENROUTER_API_KEY): ********
set provider "openrouter" with OPENROUTER_API_KEY key (length 73)? [y/N] y
configured: provider=openrouter key_env=OPENROUTER_API_KEY (run `deputyctl model test` to round-trip)
$ printf '%s' "$OPENROUTER_API_KEY" | deputyctl model set --provider openrouter --key-from-stdin --yes
configured: provider=openrouter key_env=OPENROUTER_API_KEY (run `deputyctl model test` to round-trip)
Exit codes
0— wrote the secret + active-provider pointer.1— empty key, user aborted, or write failed.64— unknown provider id, or non-TTY without--provider/--key-from-stdin.
Files read / written
- Read:
DEPUTYOS_PROVIDERS_FILE, existingDEPUTYOS_SECRETS_FILE. - Written:
DEPUTYOS_SECRETS_FILE(mode 0600),DEPUTYOS_ACTIVE_PROVIDER_FILE.
Related
model test
Single-token round-trip test against the current config.
Synopsis
deputyctl model test [--provider <id>] [--json]
Options
--provider <id>— test a specific provider id instead of the active one.--json— JSON output.
Behavior
- Resolve provider id:
--providerflag wins, elseDEPUTYOS_ACTIVE_PROVIDER_FILE. - If unresolved, error: “no active provider — run
deputyctl model set”. - Look up
key_env_varin the loaded providers file; read the value fromDEPUTYOS_SECRETS_FILE. Empty → error. - Issue a 1-token round-trip via
model::test_provider_key:- For
openai-compatibleproviders:POST <endpoint>/chat/completionswithmax_tokens=1and a tiny prompt. - For other kinds: provider-specific minimal request.
- For
- Capture HTTP status, elapsed ms, ok/fail, error string.
The same model::test_provider_key is called by the deputywizard
so both surfaces produce identical round-trip semantics.
Examples
$ deputyctl model test
provider: openrouter
endpoint: https://openrouter.ai/api/v1
http: 200
elapsed: 312 ms
result: ok
{
"provider": "openrouter",
"endpoint": "https://openrouter.ai/api/v1",
"http_status": 200,
"elapsed_ms": 312,
"ok": true,
"error": null
}
Exit codes
0— round-trip ok.1— round-trip failed (auth, network, 5xx) or no active provider configured.
Related
update
Update client.
Synopsis
deputyctl update --check [--json]
deputyctl update --apply [-y|--yes] [--json]
Options
--check— read the signed manifest from CDN and report what would change.--apply— apply an update (A/B swap with watchdog rollback is planned; today, stage toDEPUTYOS_STAGING_DIR).-y, --yes— skip the--applyconfirmation prompt.--json— structured JSON output.
--check and --apply are mutually exclusive. Passing both — or neither —
exits 64.
Behavior (—check)
- Resolve the manifest URL with the documented precedence:
DEPUTYOS_UPDATE_URL/etc/deputyos/update-url- dev fallback
file://./dist/manifest.json
- Resolve the public key:
DEPUTYOS_UPDATE_PUBKEY/etc/deputyos/pubkey.minisign./dist/pubkey.minisign~/.config/deputyos/dev-keys/deputyos-dev.pub
- Fetch manifest, fetch sidecar
<url>.minisig, verify withminisign -V. - Read installed version, current target (from
limits.json), active profile. - Pick the manifest’s first artefact whose
targetandprofilematch. - Compare
manifest.release_versionto installed; reportupdate_available.
Behavior (—apply)
Steps 1-5 from --check, then:
- If
release_version <= installed, print “already up to date” and exit 0. - Confirm
[y/N]unless--yes. - Resolve URLs against the manifest origin (
urlfield, elsefilename). - Download artefact +
.minisigtoDEPUTYOS_STAGING_DIR. - Verify SHA256 against
artefact.sha256. - Verify minisign signature against the same pubkey.
- Fire
update-appliedhooks (best effort). - Print “staged at
” — actual A/B swap is not yet shipped.
Status Today,
update --applyends at “staged” with a clear pointer todocs/08-update-rollback.md. The integrity guarantees (SHA + minisign) are real; the bootloader swap is not yet wired.
Examples
$ deputyctl update --check
manifest: https://cdn.deputyos.com/dev/manifest.json
channel: dev
installed: 2026.4.20
latest: 2026.4.27
status: update available
target: qemu-aarch64
profile: openclaw
artefact: deputyos-openclaw-qemu-aarch64-2026.4.27.qcow2 (qcow2)
size: 1873294848 bytes
sha256: 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
{
"ok": true,
"installed_version": "2026.4.20",
"latest_version": "2026.4.27",
"channel": "dev",
"update_available": true,
"target": "qemu-aarch64",
"profile": "openclaw",
"matching_artefact": { ... },
"manifest_origin": "https://cdn.deputyos.com/dev/manifest.json"
}
Exit codes
0— up to date, or check completed, or apply staged.1— verification failed, fetch failed, no matching artefact, user aborted.64—--checkand--applyboth passed, or neither passed.
Files read / written
- Read:
update-url,pubkey.minisign,version,active-profile,limits.json. - Written: under
DEPUTYOS_STAGING_DIR(applyonly).
Related
rollback
Force boot into the inactive A/B slot on next reboot.
Synopsis
deputyctl rollback
Behavior
- Read
DEPUTYOS_SLOTS_FILE(/var/lib/deputyos/slots.json). - Validate the JSON shape and the recorded SHA256 of the inactive slot’s image (real cryptographic check — corrupted rollback target caught here, not after reboot).
- Refuse to perform the swap, print which slot/version would be active, and exit 64 with a clear “not yet shipped” message.
The integrity check is real and load-bearing for the integrity contract. The swap’s swap implementation will layer on top without changing this validation logic.
Slots file shape
{
"active": "A",
"inactive": "B",
"version_a": "2026.4.27",
"version_b": "2026.4.20",
"inactive_sha256": "9f86d081...",
"inactive_image_path": "/var/lib/deputyos/slot-b.img"
}
Examples
$ deputyctl rollback
rollback: inactive slot integrity verified (9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08)
rollback: would roll back from slot A to slot B (version 2026.4.20)
rollback: A/B swap implementation is not yet shipped — see docs/08-update-rollback.md
Exit codes
0— never (today). It will return 0 on success once shipped.64— rollback target not provisioned, or the swap not yet shipped.- (anyhow error) — slots.json malformed, or integrity check failed.
Related
backup now
One-off rclone push to the configured destination.
Synopsis
deputyctl backup now [--dry-run]
Options
--dry-run— pass--dry-runto rclone.
Behavior
- Verify
rcloneis on PATH. - Load
DEPUTYOS_BACKUP_CONFIG(TOML;[backup].remoterequired). - Load active profile manifest; read
[paths].data_dir. - Build destination:
<remote>/<hostname>/<utc-timestamp>/. - Run
rclone sync <data_dir> <dest> --config <DEPUTYOS_RCLONE_CONFIG>(with--dry-runif requested). - Print snapshot id, dest, source.
Hostname resolves from /etc/hostname → $HOSTNAME → deputyos.
Examples
$ deputyctl backup now
backup now: rclone sync /home/agent/.openclaw r2:deputyos-backups/myhost/1714137600T-utc/ (config /etc/deputyos/rclone.conf)
... rclone progress ...
snapshot: myhost-1714137600T-utc
dest: r2:deputyos-backups/myhost/1714137600T-utc/
source: /home/agent/.openclaw
Exit codes
0— sync succeeded.1— rclone missing, config missing/invalid, or sync exited non-zero.
Related
backup schedule
Configure a cron-like schedule.
Synopsis
deputyctl backup schedule [--every <interval>] [--at <HH:MM>] [--list] [--disable]
Options
--every <interval>—6h,30m,1d. Default if neither--everynor--at:--every 6h.--at <HH:MM>— daily at this local time. Mutually exclusive with--every.--list— print the current schedule’sOnCalendar=line.--disable— stop and remove the timer.
Behavior
Writes deputyos-backup.timer and deputyos-backup.service under
DEPUTYOS_SYSTEMD_UNIT_DIR, then runs systemctl daemon-reload +
systemctl enable --now deputyos-backup.timer. The service simply runs
deputyctl backup now (path inferred from current_exe).
In dev mode (DEPUTYOS_DEV_OUT=<dir>), units are written under
<dev-out>/systemd/ and systemctl is not invoked, so a contributor
laptop never installs system-wide units.
--every-to-OnCalendar mapping:
--every | OnCalendar= |
|---|---|
6h | *-*-* 00/6:00:00 |
30m | *:0/30:00 |
1d | *-*-* 03:00:00 |
Examples
$ deputyctl backup schedule --every 6h
backup schedule: wrote /etc/systemd/system/deputyos-backup.timer
backup schedule: wrote /etc/systemd/system/deputyos-backup.service
scheduled: OnCalendar=*-*-* 00/6:00:00
$ deputyctl backup schedule --list
OnCalendar: *-*-* 00/6:00:00
$ deputyctl backup schedule --disable
backup schedule: removed /etc/systemd/system/deputyos-backup.timer
backup schedule: removed /etc/systemd/system/deputyos-backup.service
disabled
Exit codes
0— schedule wrote / disabled / listed.1(anyhow) —--everyand--atboth passed, bad interval, or write failed.
Files written
DEPUTYOS_SYSTEMD_UNIT_DIR/deputyos-backup.timerDEPUTYOS_SYSTEMD_UNIT_DIR/deputyos-backup.service
Related
restore list
List snapshots in the user bucket.
Synopsis
deputyctl restore list [--json]
Options
--json— JSON output.
Behavior
rclone lsd <remote>/<hostname> then parse the listing into snapshot
records. Each record has id, size, and an iso_timestamp reconstructed
from rclone’s date/time columns.
Examples
$ deputyctl restore list
myhost-1713964800T-utc ? 2026-04-25T03:00:00Z
myhost-1714051200T-utc ? 2026-04-26T03:00:00Z
[
{
"id": "myhost-1713964800T-utc",
"size": "?",
"iso_timestamp": "2026-04-25T03:00:00Z"
}
]
Exit codes
0— listing succeeded (even if empty).1— rclone missing, config missing, or lsd failed.
restore snapshot
Atomic restore from a snapshot id.
Synopsis
deputyctl restore snapshot --snapshot <id> [-y|--yes]
Options
--snapshot <id>— id fromrestore list.-y, --yes— skip confirmation.
Behavior
- Stop the active profile’s unit (best effort).
- Move the current
data_diraside to<data_dir>.pre-restore-<epoch>. rclone copy <remote>/<host>/<id>/ <data_dir>.- Roll back the move-aside if rclone fails.
- Start the active profile’s unit.
The rollback path is real — it’s why the move-aside happens before the
copy. On a successful restore the .pre-restore-* dir is left in place
(disk insurance) for the user to delete manually.
Destructive
restore snapshotreplaces the entiredata_dir. This includes all conversations, learned skills, and per-profile state. The.pre-restore-*sidecar is your only undo button; it’s not auto-cleaned.
Examples
$ deputyctl restore snapshot --snapshot myhost-1713964800T-utc
this will replace the current data dir at /home/agent/.openclaw with snapshot myhost-1713964800T-utc; continue? [y/N] y
... rclone copy progress ...
restored: snapshot myhost-1713964800T-utc into /home/agent/.openclaw (rollback copy at /home/agent/.openclaw.pre-restore-1714137600)
Exit codes
0— restore succeeded.1— rclone missing, copy failed (rolled back), user aborted.64— empty snapshot id.
Related
factory-reset
Wipe data partition; keep system partitions intact.
Synopsis
deputyctl factory-reset [-y|--yes] [--dry-run]
Options
-y, --yes— skip the typed-confirmation prompt.--dry-run— print planned actions without making changes.
Behavior
- Build the wipe plan:
data_dir,secrets.env,active-profile,wizard-state.json. - Print the plan (preserving
~/.ssh/authorized_keys). - If
--dry-run: exit 0. - Unless
--yes: prompt for the literal phrasereset deputyos. Anything else aborts with exit 1. - Stop the active profile’s unit.
- Wipe
data_dirrecursively, preserving~/.ssh/authorized_keys(read into memory, dir wiped, restored with original mode). - Truncate
secrets.env(preserving mode0600). - Remove
active-profileandwizard-state.jsonso first-boot UX repeats.
In dev mode (DEPUTYOS_DEV_OUT=<dir>) the wipe targets only paths under
<dev-out>/, never the real system. This is what the test harness uses.
Destructive Removes all conversations, model keys, learned skills. Designed to leave SSH access intact for recovery and to force the wizard on next boot.
Examples
$ deputyctl factory-reset --dry-run
plan:
- stop active profile unit
- wipe /home/agent/.openclaw
(preserving /home/agent/.ssh/authorized_keys)
- truncate /etc/deputyos/secrets.env
- remove /etc/deputyos/active-profile
- remove /var/lib/deputyos/wizard-state.json
(dry-run) no changes made
$ deputyctl factory-reset
plan: ...
This wipes all conversations, model keys, and learned skills. Are you sure?
Type 'reset deputyos' to confirm: reset deputyos
factory-reset complete; reboot or run `deputyctl init`
Exit codes
0— wipe completed (or dry-run).1— confirmation phrase mismatch, or wipe step failed.
Files read / written
- Read: active profile manifest (for data_dir).
- Wiped/truncated:
data_dir/*,secrets.env,active-profile,wizard-state.json. - Preserved:
~/.ssh/authorized_keys.
Related
tunnel
Open a Cloudflare Quick Tunnel and print the URL.
Synopsis
deputyctl tunnel [--port <N>] [--background]
Options
--port <N>— local port to expose. Default8088(the wizard port).--background— detach cloudflared and write its PID to/run/deputyos/cloudflared.pid(overridable byDEPUTYOS_CLOUDFLARED_PID).
Behavior
- Verify
cloudflared(orDEPUTYOS_CLOUDFLARED) is on PATH. - Spawn
cloudflared tunnel --url http://localhost:<port>. - Read stderr line-by-line until a
https://*.trycloudflare.comURL appears. - Print the URL on stdout (machine-friendly).
- Foreground mode: continue forwarding cloudflared’s stderr to the user’s stderr. Background mode: write the PID file and return.
Examples
$ deputyctl tunnel
https://random-words.trycloudflare.com
2026-04-26T12:00:01Z INF Connection established
...
$ deputyctl tunnel --background --port 8089
tunnel: spawned cloudflared (pid 12345)
https://other-words.trycloudflare.com
$
Exit codes
0— cloudflared printed a URL and (foreground) exited cleanly, or (background) we detached.1— cloudflared not installed, or foreground exit non-zero.
Related
cost
Cost guardrails: per-day + per-month caps with auto-pause.
Surface extension
costandquiet-hoursare NOT in the frozen command surface indocs/02-profiles.md. Both were added in a later release.
Synopsis
deputyctl cost [--json] [--check]
deputyctl cost set [--daily-cap <USD>] [--monthly-cap <USD>] [--on-cap-trip <pause|warn|nothing>] [--warn-at-pct <0..=100>]
deputyctl cost reset
deputyctl cost ledger [--last <N>] [--json]
Default invocation
deputyctl cost (no subcommand) prints the summary: today/month USD spend
vs cap, on_cap_trip behaviour, warn threshold, tripped flag, and the top
5 most expensive recent ledger entries. With --json you get a structured
payload; with --check it runs the gate and exits 1 if tripped (no
output unless something changed).
cost summary JSON shape
{
"today_usd": 1.234,
"month_usd": 12.345,
"daily_cap_usd": 5.00,
"monthly_cap_usd": 100.00,
"daily_pct": 24.7,
"monthly_pct": 12.3,
"daily_state": "ok", // "ok" | "warn" | "tripped"
"monthly_state": "ok",
"tripped": false,
"on_cap_trip": "pause",
"warn_at_pct": 80,
"ledger_path": "/home/agent/.openclaw/cost-ledger.jsonl",
"config_path": "/etc/deputyos/cost.toml",
"recent_expensive": [ ... up to 5 LedgerEntry objects ... ]
}
cost set
Updates cost.toml (atomic, mode 0600). All flags are optional but at
least one must be passed (else exit 64). on_cap_trip must be one of
pause, warn, nothing. warn_at_pct must be 0..=100. Negative caps
are rejected.
$ deputyctl cost set --daily-cap 10 --monthly-cap 200 --on-cap-trip pause --warn-at-pct 90
daily_cap_usd: $10.00
monthly_cap_usd: $200.00
on_cap_trip: pause
warn_at_pct: 90%
wrote: /etc/deputyos/cost.toml
cost reset
Clears the tripped marker file (DEPUTYOS_COST_TRIPPED, default
/var/lib/deputyos/cost-tripped). Does not touch the ledger.
Idempotent.
cost ledger [--last N]
Reads DEPUTYOS_COST_LEDGER (a JSONL file the agent profile writes), sorts
by timestamp descending, prints the last N (default 20). With --json,
emits the array.
Ledger entry schema
{
"timestamp": "2026-04-26T12:34:56Z", // RFC3339 UTC
"provider": "openrouter",
"model": "anthropic/claude-3.5-sonnet",
"input_tokens": 1234,
"output_tokens": 567,
"cost_usd": 0.0123,
"request_id": "req_abc123"
}
The agent profile is the producer of these entries. deputyctl is
read-only on the ledger (except for cost reset, which only clears the
marker). Missing cost_usd can be filled by the consumer using
DEPUTYOS_COST_DEFAULTS rate sheets.
Cap evaluation
- Daily — sum
cost_usdof entries whosetimestampstarts with today’sYYYY-MM-DD(UTC). Compare tocaps.daily_usd. - Monthly — sum
cost_usdof entries whosetimestampstarts withYYYY-MM(UTC). Compare tocaps.monthly_usd. - A cap of
0.0means “disabled”. - States:
Ok<caps.warn_at_pct%of cap;Warnbetweenwarn_at_pctand 100;Tripped≥ 100%.
When a Tripped state is observed, deputyctl fires the
CostAlert hook (see hook-payloads schema) (level=trip)
and — if on_cap_trip == "pause" — writes the tripped marker and runs
systemctl stop on the active unit (best effort; in dev mode the systemctl
call is skipped).
Examples
$ deputyctl cost
today: $1.2340 / $5.00 (25% — ok)
month: $12.3450 / $100.00 (12% — ok)
on_cap_trip: pause
warn_at_pct: 80%
tripped: false
recent (top 5 by cost):
$ 0.0820 2026-04-26T12:34:56Z openrouter anthropic/claude-3.5-sonnet
...
$ deputyctl cost --check
$ echo $?
0
Exit codes
0— summary, set, reset, ledger ok, or--checkreports not tripped.1—--checkreports tripped, or write failed.64—cost setwith no fields changed.
Related
quiet-hours
Quiet-hours schedule: when to pause / refuse messages.
Surface extension See note under
cost— same authorisation, samecost.tomlconfig file.
Synopsis
deputyctl quiet-hours [--json]
deputyctl quiet-hours set [--start <HH:MM>] [--end <HH:MM>] [--enable] [--disable] [--behaviour <pause|refuse|nothing>]
Default invocation
deputyctl quiet-hours shows the current schedule + whether it’s active
right now:
enabled: true
start: 22:00 (local)
end: 07:00 (local)
behaviour: pause
active_now: false
config: /etc/deputyos/cost.toml
{
"enabled": true,
"start": "22:00",
"end": "07:00",
"behaviour": "pause",
"active_now": false,
"config_path": "/etc/deputyos/cost.toml"
}
quiet-hours set
Updates the [quiet_hours] section of cost.toml (atomic, mode 0600).
At least one of --start, --end, --enable, --disable, --behaviour
must be passed (else exit 64). --enable and --disable are mutually
exclusive. --behaviour ∈ {pause, refuse, nothing}. Times are HH:MM
in local time (24-hour).
Time semantics
- The window is closed-open
[start, end).start == endis treated as empty (24-hour silence is not what the user means; they’d disable instead). - Cross-midnight windows are supported: if
start > end, the window is[start, 1440) ∪ [0, end). - “Local time” resolves via
TZenv (UTC,+05:30,-0800, etc.) →/etc/timezone→date +%z→ UTC. DST is honoured to the extent the system’sdatecommand honours it.
Examples
$ deputyctl quiet-hours set --start 22:00 --end 07:00 --enable --behaviour pause
enabled: true
start: 22:00
end: 07:00
behaviour: pause
wrote: /etc/deputyos/cost.toml
$ deputyctl quiet-hours set --disable
enabled: false
...
Exit codes
0— show / set ok.64—--enableand--disableboth passed, or no fields changed.
Related
- operations/cost-guardrails (same config file)
--internal-run-relay
Internal — not part of the public surface
--internal-run-relay <SOCKET>is a hidden top-level flag, intentionally omitted from--helpand from the frozen subcommand surface indocs/02-profiles.md. It exists so the systemd unit fronting the message relay can run deputyctl as the relay server. Documented here for transparency; do not script against it.
Synopsis
deputyctl --internal-run-relay <SOCKET-PATH>
Behavior
- Initialize tracing.
- Bind a
SOCK_STREAMUnix-domain socket at<SOCKET-PATH>(orDEPUTYOS_RELAY_SOCKETif no path is supplied; default/run/deputyos/relay.sock). Mode0600. - Accept connections one-at-a-time. Each connection is one newline-delimited
JSON request:
{ "kind": "pre-message" | "post-message" | "cost-alert" | "update-applied", "payload": { ... } } - Dispatch to
crate::hooks::fire_hook_in_collectagainstDEPUTYOS_HOOKS_DIR. - Reply with one JSON line:
{ "ok": true|false, "errors": [{"script": "...", "code": 0, "stderr_tail": "..."}, ...] } - Close the connection. Loop forever.
The argv-scan happens before clap parses, so this flag is not visible
to --help and does not appear in the [Command] enum.
Files read / written
- Read:
DEPUTYOS_HOOKS_DIR/<kind>/*executable scripts. - Written: socket file at the supplied path (mode
0600).
Related
See also
- deputywizard — first-boot wizard binary.
- deputypwa — always-on companion PWA.
- deputyos-track — release tracker bot.
- deputyos-desktop — desktop launcher.
- filesystem-layout — every path deputyctl reads or writes.
- systemd-units — every unit deputyctl manipulates.
- Profile TOML schema
- providers JSON schema
- limits JSON schema
- release-manifest schema
- hook-payloads schema
- How to add a profile
- How to rotate keys
- How to backup and restore
- operations/cost-guardrails
- operations/update-and-rollback
- security/update-trust-chain