crewship admin
cmd/crewship/cmd_admin.go exposes the operator-on-the-host recovery surface. The write subcommands under admin open the SQLite database file on this machine directly (no HTTP, no auth token) — the server doesn’t even need to be running. Use these when a user is locked out of the UI.
The “credential” for those commands is shell access to the host. That matches what GitLab (gitlab-rake gitlab:password:reset), Gitea (gitea admin user change-password), Nextcloud (occ user:resetpassword), and Mattermost (mmctl user change-password) all do — if you can SSH to the box, you ARE the admin.
Which database am I talking to?
admin list-users reads the server the CLI targets. Everything else in
the local-only group reads or writes the database file on this host.
Those are two different things, and until #2086
the CLI conflated them: it resolved ~/.crewship/crewship.db and ignored
--server / CREWSHIP_SERVER / --profile entirely, so pointing the CLI at
a populated server and running admin list-users printed
list-usersusesGET /api/v1/admin/userson the targeted server: the current workspace’s members, or every account for the instance owner. It needs a login — always, not only when something names a server. It is the one verb here that is not gated: it branches on--localalone, so on a host with no CLI config and noCREWSHIP_SERVERit still goes to the server and still fails with “not logged in”, where the pre-#2086 build printed the table. Deliberate — a fallback to the local file on a failed login would answer the server’s question from a file that may belong to another instance.--localis the old behaviour, asked for explicitly, and both failure paths say so.- Every other local-only subcommand refuses to run when a server is named,
unless you pass
--local. The refusal names the server, names the file it would otherwise have touched, and names the flag. With nothing naming a server they run unchanged. - A server on
localhostis not an exemption. crewshipd routinely runs with its ownDATABASE_URL— every dev clone usesfile:./crewship.db— so “same host” tells you nothing about “same database”.
The credential for the local-only commands is shell access to the host —
there is no
crewship login token. Requirements:- Read+write access to the database file:
DATABASE_URLwhen set, otherwise<data dir>/crewship.db(default~/.crewship). - The
crewshipbinary must run on the same host as that file. - The host’s filesystem permissions are the authentication.
--local is what tells the CLI you mean that file rather than the server it
is configured for. Every command prints the path it resolved.Subcommands
crewship admin reencrypt
Re-encrypt stored secret envelopes to the server’s current master-key version.
This is the server-side step after configuring a new key version; the command
requires authentication, an active workspace, and a running server.
key_version, totals for
Re-encrypted, Skipped, and Failed, followed by per-table/per-column
counts when present. JSON output contains key_version, reencrypted,
skipped, failed, and columns. A non-zero exit means at least one value
could not be decrypted with the configured keys; do not retire needed keys.
Even with Failed: 0, keep the exact original ENCRYPTION_KEY: journal and
instance-audit MACs retain this v1 root. Re-encryption does not re-sign history.
crewship admin reset-password
Rotate a user’s password.
crewship admin list-users
Two different answers, deliberately:
Default — the server. GET /api/v1/admin/users on the server the CLI
targets (ADMIN+): the members of the current workspace, or — for an instance
administrator — every account on the instance. Columns: EMAIL, NAME, CREATED, ROLE
(role@workspace-slug, comma-joined), LAST ACTIVE, SESSIONS (signed-in
devices), LOCKED (until … while a lockout is in force). Requires a login.
--local — the database file on this host. Every row in users ordered
by created_at ASC, across every workspace. Columns: EMAIL, NAME, CREATED,
LOCKED (derived from locked_until), FAILS (failed_login_count), ROLES
(role@workspace-slug, comma-joined across workspaces). Needs no server —
the path for when the server or the login is what is broken.
To lift a lock from the terminal:
crewship admin user unlock <email> (over
HTTP), or reset-password --local on the database host.
Notes go to stderr: which people the list covers on the server path, and
the “which database” note on the --local path. Stdout carries the table and
nothing else, so crewship admin list-users | awk 'NR>1 {print $1}' yields
addresses.
That holds for an empty workspace too: stdout is the header row alone and
(no users in this workspace) is printed on stderr, so the same pipe yields
nothing rather than a line of prose. (The --local listing keeps its own
(no users — run 'crewship seed' …) on stdout — it is the example output
shown in the admin CLI guide.)
crewship admin promote
Grant a workspace role to an existing user. Roles ascend VIEWER < MEMBER < MANAGER < ADMIN < OWNER; the command updates the user’s existing row in workspace_members (the user must already be a member — a non-member errors with user is not a member of workspace).
crewship workspace member role, which is the
server-side way to do the same thing. That path enforces the role ladder
against your role and needs a session — so it cannot mint the first OWNER,
and it is unavailable to the operator who is locked out. When the CLI names a
server, admin promote refuses and points at both options.
This is the post-
init step for bootstrapping a second admin: the first user from crewship init is OWNER automatically; everyone else starts as a regular member and needs admin promote to gain elevated access without going through the UI.
crewship admin invalidate-sessions
Force-logout every active session for one user without changing their password. The user can still sign in normally afterwards — they just have to re-authenticate on every device they were already on.
Each revoked row is stamped
revoked_reason='admin_invalidate' so the audit trail distinguishes this from the side-effect revokes that fire during reset-password (those carry revoked_reason='password_change').
When to reach for this instead of reset-password
When to reach for this instead of reset-password
- Laptop stolen / recovered, the password is believed safe but a cached cookie might still be on the device.
- Suspected token leak via Slack screenshot, browser history dump, etc.
- Periodic compliance sweep — “log everyone out of yesterday’s sessions”.
crewship admin sessions list
Forensic read of user_sessions for one user — mirrors crewship session list but for arbitrary users (the user-side command is self-only). Admin-only via direct DB access.
Output columns: ID, STATUS (derived from
revoked_at + expires_at; revoked rows show revoked:<reason>), CREATED, LAST USED (last_used_at), EXPIRES, IP, UA (user_agent). Sorted created_at DESC. A typo’d email reports no user with email <addr> rather than silently returning zero rows.
Pair with admin invalidate-sessions to act on what sessions list surfaces.
crewship admin prune-legacy
Removes orphaned pre-C1 (slug-only) crew docker resources — volumes and
containers named <prefix>-{home,tools,team}-<slug> (e.g.
crewship-3-tools-engineering) left over from before the C1 naming change
(2026-06 audit, which re-keyed crew resources to include the crew id).
Why this exists
A legacy slug-only volume survivescrewship seed --nuke: the nuke clears
the database, and crew teardown removes the id-scoped volumes — but never the
orphaned slug-only ones, because no crew row references them. While they exist,
the runtime’s legacy-resource guard refuses to start the crew’s container, and
the failure reaches users only as a generic “failed to start agent
container”. Every agent in the affected crew fails; a seed --smoke-test
reports N/N agents failed.
crewship doctor surfaces this proactively: the legacy crew resources check
calls the authenticated GET /api/v1/admin/legacy-resources endpoint and
WARNs with this command as the remediation when orphaned resources are
present. (Detection runs on this admin endpoint, not on the unauthenticated
/healthz hot path, so a slow docker daemon can never stall health probes.)
The prune is instance-wide: legacy docker names carry no workspace or crew
id, so detection (what crewship doctor surfaces) and prune enumerate the same
full crew set — otherwise the doctor could WARN on a slug the prune can never
reach. It removes only the orphaned legacy names; the id-scoped resources
the live runtime uses are excluded (slug/id-collision safe) and never touched.
It reports each resource removed:
OWNER or ADMIN role. Returns 503 when the server’s container
provider is not docker (nothing to prune).
crewship admin reap-orphan-containers
Finds — and, with --apply, reaps — crew containers orphaned by an
internal-token master rotation. When crewshipd restarts with a different
internal-token master while a per-crew agent container survives, that container
keeps the crew-bound token (crwv1.<ws>.<crew>.<mac>) it was minted with, and
the new process rejects it forever:
docker ps still shows it “healthy”, and it re-emits the warning every reap
interval (log-noise that drowns real signal). This command clears those
orphans.
Reading the result: “none found” vs “could not tell”
The detector compares the token fingerprint a container’s sidecar advertises on/health against the one the server would mint for that crew today. It fails
safe: a container that advertises no fingerprint is never classified as an
orphan, so a reap only ever removes something positively proven stale.
That safety has a corollary — a sidecar too old to advertise token_fp at all
makes the detector structurally unable to find anything, while still
reporting zero orphans. So the sweep also reports its own coverage:
--format json carries inspected, identified and detector_inert. A server
predating these fields omits them, and the CLI then falls back to the older,
non-committal wording rather than inferring a coverage figure it wasn’t given.
One-time step after upgrading to the stable-master build
The stable internal-token master (#1385) stops future restarts from ever orphaning a container. But the deploy that first introduces it rotates the master once — from the old per-boot random to the value derived fromENCRYPTION_KEY. Any container that
outlived that one deploy is orphaned exactly once and does not self-heal.
After rolling out the new build, do one of:
- Recreate the surviving containers — a normal redeploy / crew re-dispatch starts a fresh container that re-mints a valid token; or
- Run
crewship admin reap-orphan-containers --applyto stop+remove just the orphaned ones (the next dispatch to each crew recreates them fresh).
CREWSHIP_INTERNAL_TOKEN or the ENCRYPTION_KEY it derives
from.
How detection stays safe
For each of the workspace’s crews the command probes the running container’s sidecar/health for the token fingerprint it advertises (token_fp, #1385)
and compares it against the fingerprint of the token the server would mint
today. A container is listed only on a definite, non-empty mismatch —
an unreachable sidecar, a pre-#1385 sidecar that reports no fingerprint, or an
unconfigured master are all treated as unknown and are never reaped. A
healthy container is never touched. Any agent inside an orphaned container is
already broken (its credentials can’t sync), so recreation restores it rather
than interrupting healthy work.
Dry-run prints the orphans it found; --apply reports what it reaped:
OWNER or ADMIN role. Returns 503 when the server’s container
provider is not docker.
See also
crewship init— first-user bootstrap on a fresh database. Afterinit,admin promoteis how you add a second OWNER.crewship doctor— thelegacy crew resourcescheck WARNs when a prune is needed.crewship session— user-scoped self-service of the sameuser_sessionstable.crewship doctor— adjacent host-side diagnostics (data dir, schema version, container runtime).
crewship admin health
ENCRYPTION_KEY came from. generated means
Crewship auto-created it into <dataDir>/secrets.env, next to the database it
protects — so a copied disk, a restored snapshot or a stray backup carries the
ciphertext and what opens it. external means the operator injected it.
Log level shows the live level, and says (temporary) when a timed
override is in effect — a level that reverts on its own is a different fact
from one someone set for good.
crewship admin stats
crewship admin workspaces
GET /api/v1/admin/workspaces. A workspace ADMIN or OWNER gets one row, the
workspace the CLI is authenticated against. An instance administrator (see
crewship admin instance) gets every workspace on the instance; * marks the current one. KIND is team or personal (created at
sign-up for one person; update-workspace changes it). RUNS 7D counts runs started in the
last seven UTC days, SPEND 30D is the estimated spend over the last 30 days,
LAST ACTIVITY is the newest audit-log entry. A server older than those fields
prints - for them.
Requires OWNER or ADMIN (canRole "manage").
crewship admin user
Per-person actions on the server the CLI targets. People are named by email;
the id is resolved through GET /api/v1/admin/users, so you can act on the
members of your current workspace, or on any account if you are an instance
administrator. An ADMIN cannot act on an OWNER.
The host-side
invalidate-sessions and sessions list stay for when the
server or the login is what is broken; these are the same actions over HTTP,
for an admin who is signed in. Every action writes an audit-log entry.
crewship admin instance
What an instance administrator does across the whole server. People are named
by email and workspaces by slug. Only an instance administrator may run
these: CREWSHIP_OWNER_EMAIL on the server, or anyone named with add-admin.
An install that names nobody names the owners of its oldest workspace once,
the first time it is asked, and never again. Being ADMIN or OWNER of a
workspace is not enough. Every action is recorded in the instance audit. The
last active instance administrator cannot be removed or suspended. These
commands need no workspace selected (crewship workspace use): an instance
administrator may belong to none.
add-person <email>
POST /api/v1/admin/instance/people. Creates the account, puts it in the
workspaces you name and prints a setup link valid for 7 days. No email is
sent; pass the link on yourself. The person chooses their own password.
grant <email>
PUT /api/v1/admin/instance/workspaces/{workspaceId}/members/{userId}. Gives
access to a workspace or changes the role there. OWNER is allowed; the last
owner of a workspace cannot be moved down, use transfer instead.
revoke <email>
DELETE /api/v1/admin/instance/workspaces/{workspaceId}/members/{userId}.
Takes the person out of the workspace. Their pages move first, as when a
workspace admin removes them. The last owner stays.
create-workspace <name>
POST /api/v1/admin/instance/workspaces. Creates an empty workspace owned by
--owner. You are not added to it.
update-workspace <slug>
PATCH /api/v1/admin/instance/workspaces/{workspaceId}. Renames any
workspace, or marks it team (shared) or personal (created at sign-up for
one person). You are not added to it, and the slug stays.
Give at least one of them.
transfer <workspace-slug>
POST /api/v1/admin/instance/workspaces/{workspaceId}/transfer-ownership.
Makes a member the owner; every previous owner stays on as ADMIN.
delete-workspace <slug>
DELETE /api/v1/admin/instance/workspaces/{workspaceId}. Deletes the
workspace with its crews and agents. The instance audit keeps the record.
suspend <email>
POST /api/v1/admin/instance/people/{userId}/suspend. The account can no
longer sign in; every session and CLI token ends now. Workspace access is
kept for reactivate. You cannot suspend yourself or the instance owner.
reactivate <email>
POST /api/v1/admin/instance/people/{userId}/reactivate. Lifts a suspension.
Ended sessions stay ended; the person signs in again.
setup-link <email>
POST /api/v1/admin/instance/people/{userId}/setup-link prints a new setup
link and voids the previous one. Only an account nobody has signed in to gets
one; anyone else resets their own password.
add-admin <email>
PUT /api/v1/admin/instance/admins/{userId}. Names an instance administrator.
The named admins and CREWSHIP_OWNER_EMAIL are the whole list. When nobody
who could run this can sign in, --local writes the database file on the
host — the one way back once the last admin is gone. Stop the server first: a
running server owns the only database writer, and the command refuses beside it.
remove-admin <email>
DELETE /api/v1/admin/instance/admins/{userId}. Removes a named instance
administrator. Nobody removes themselves; CREWSHIP_OWNER_EMAIL is changed in
the server’s environment.
audit
GET /api/v1/admin/instance/audit. Instance-level actions, newest first.
keeper governance
GET /api/v1/admin/instance/keeper/governance. What each workspace has
switched on: the watchdog, its sampling, the DENY alert threshold, four-eyes
approval, the credential lease and the judge. Every workspace is listed,
including ones you are not a member of. default in the SOURCE column means
the workspace was never set and follows the instance defaults — the last save
for all workspaces.
keeper set
PUT /api/v1/admin/instance/keeper/governance. Sets the Keeper watchdog for
one, several or all existing workspaces. Only the settings you pass change. For one
workspace it saves straight away. For several, or --all, it first lists
every workspace whose settings would be overwritten and asks, then saves
exactly that: if a workspace was created or a setting changed in between, the
server refuses and nothing is written. The save is all or nothing: if one workspace refuses the values, none changes. --all means
every existing workspace; what a new workspace starts with is keeper defaults.
keeper defaults
GET | PUT /api/v1/admin/instance/keeper/governance/defaults. The template a
new workspace’s Keeper starts with: a workspace copies it when it is created
and keeps its copy, so changing the template changes no existing workspace
(keeper set --all does that). Without settings it shows the template; with
them it shows the change, asks, and saves exactly what it showed.
keeper requests
GET /api/v1/admin/instance/keeper/requests. Keeper decisions across every
workspace, newest first: credential requests and the background reviews, each
row with its workspace. The last line totals what the filter matches.
keeper health
GET /api/v1/admin/instance/keeper/health. Every workspace’s recent decision
window: how many decisions it holds, how many let work go on, how often the
judge failed, the p95 latency and any standing alarm. Below the sample minimum
there is too little to judge, which is not the same as healthy.
retention get
GET /api/v1/admin/instance/retention. How long every workspace keeps each
kind of data: routine runs, approvals, audit log, credential audit, memory
versions, page panel data, inbox items, chats and Keeper decisions. A window
is a number of days or forever. The last row is what a workspace created now
starts with (see retention defaults), followed by the fixed limits the server applies on its own. Use
-f json for the full document, including what each window deletes.
retention set
PUT /api/v1/admin/instance/retention. Sets retention windows for one,
several or every existing workspace. Only the windows you pass change. Each takes a
number of days, 1–3650, or forever; routine runs, memory versions and page
panel data always age out and cannot be forever.
It always shows first what would change and how many rows the next daily
sweep would then delete, and asks. It saves exactly what it showed: if a
workspace was created or a window changed in between, the server refuses and
nothing is written. The save is all or nothing, and saving a window deletes
nothing by itself: the sweep does, at its next run. Backups taken before still
hold the rows it removes. --all selects every workspace that exists now; it
does not change what a workspace created later starts with, which is
retention defaults set.
retention defaults get
GET /api/v1/admin/instance/retention/defaults. The retention windows a
workspace created now starts with, one row per window. SOURCE says whether an
administrator set it or it is the product default.
retention defaults set
PUT /api/v1/admin/instance/retention/defaults. Changes the retention windows
a workspace created later starts with. Only the windows you pass change; each
takes a number of days, 1–3650, or forever. No existing workspace changes
and nothing is deleted. It shows the change first and asks, and the
confirmation saves exactly what it showed: if another admin changed a default
in between, the server refuses (409) and nothing is written.
backups bundles
GET /api/v1/admin/instance/backups/bundles. Every catalogued backup bundle on
the server, across every workspace — including ones you are not a member of.
Each row carries its proof level (checksum: written and its checksum
recorded; contents checked; test restore), whether it is pinned, and its
gaps: what the bundle should hold and does not — attachment files that were
not on disk, memory versions without content, provisioned crews whose
container was gone. complete means the bundle recorded no gap; not recorded means it was catalogued before gaps were recorded, which is not the
same as complete.
backups pin <path>
POST /api/v1/admin/instance/backups/bundles/pin. Pins a bundle so no
retention rule deletes it — not the age rule, not the count floor, not a
rotation. The path is the bundle’s path as backups bundles prints it. Written
to the instance audit log.
backups unpin <path>
POST /api/v1/admin/instance/backups/bundles/unpin. Removes the pin; retention
rules apply to the bundle again. Written to the instance audit log.
backups restores
GET /api/v1/admin/instance/backups/restores. Every restore, dry run and drill
the server has run, newest first: who ran it, which bundle, into what, and the
result — ok, partial (it landed, but the report names something skipped,
missing or lowered: crew files not landed, credential tiers clamped, columns
dropped, attachment files missing) or failed. --format json carries each
full report.
backups plans list
GET /api/v1/admin/instance/backups/plans. Every backup plan: its preset,
scope, when it runs and in which timezone, whether it is switched on, and its
next run time. Plans run on the server’s backup scheduler, never through an
agent.
backups plans get <id>
GET /api/v1/admin/instance/backups/plans/{id}. One plan in full: scope,
contents, schedule, keep rules and busy window.
backups plans create
POST /api/v1/admin/instance/backups/plans. --preset picks the defaults:
complete backs up the whole instance with data daily at 03:00 and complete
environments weekly; workspace backs up every category of the chosen
workspaces daily; custom backs up only the categories in --contents (the
server adds what they need to be restorable) and never counts as protecting a
workspace. --recipient is required: every backup is encrypted. Flags you do
not pass take the preset’s default. Written to the instance audit log.
backups plans update <id>
PUT /api/v1/admin/instance/backups/plans/{id}. Only the flags you pass
change, and the next run time is recomputed. Takes the same flags as
backups plans create; --disabled=false switches a plan back on. Written to
the instance audit log.
backups plans delete <id>
DELETE /api/v1/admin/instance/backups/plans/{id}. Deletes the plan. Its
bundles and its run history stay. Written to the instance audit log.
backups plans next <id>
GET /api/v1/admin/instance/backups/plans/{id}/next. The plan’s next run
times, and which of them also take complete environments.
backups plans calendar <id>
GET /api/v1/admin/instance/backups/plans/{id}/calendar. Day by day: runs
that were done, skipped, caught up or failed, and runs still planned.
backups plans preview
POST /api/v1/admin/instance/backups/plans/preview-contents. What a plan’s
backups would carry: the categories included, the ones kept because others
need them (memory needs crews and agents, attachment files need chats), and
the ones left out.
backups runs
GET /api/v1/admin/instance/backups/runs. Every backup run, newest first: the
plan, the trigger (schedule, manual, catchup), the workspace, the status
(running, done, incomplete, failed, interrupted, skipped), each
phase it went through and the bundle’s proof level.
backups run
POST /api/v1/admin/instance/backups/run. Starts a backup now in the
server’s backup service (never through an agent). The server answers with the
run id at once and finishes the backup on its own, so the CLI’s timeout
cannot cancel it; --wait follows every run it started to the end and prints
what each wrote (with --format json, a list of runs in the shape of
backups runs).
With --plan it runs that plan. Otherwise --scope instance writes one
instance bundle — the whole database, every file store and every crew
container, copied in one quiet window: running work is drained first, writes
are held for the copy only and released before packing — and
--scope workspaces writes one bundle per workspace, narrowed by
--workspace, with --preset and --contents choosing what goes in.
Every backup is encrypted: --recipient (repeatable) or --passphrase-file,
not both. Without either, the recipients come from the plan that covers the
same scope. The passphrase is held in the server’s memory until the run
starts and is never stored.
backups run-status <id>
GET /api/v1/admin/instance/backups/run/{runId}. One run, with the same
fields as a row of backups runs: its status (running, done,
incomplete, failed, interrupted or skipped), each phase, the bundle it
wrote, how long writes were held, and every gap. Exits non-zero when the run
failed, was interrupted or was skipped.
backups overview
GET /api/v1/admin/instance/backups/overview. Whether a restore would work.
The verdict is the weakest proof among the latest full backups of what you
selected — verified, partial, failed, contents_checked,
checksum_only or none; custom backups never count. Then what is
protected, how often, where, for how long, the last test restore, what needs
attention, the last 14 nights and the space backups use and need.
backups vault-keys
GET /api/v1/admin/instance/backups/vault-keys. Every vault key version the
database’s sealed values reference or this server has configured, the
environment variable it is read from, how many sealed values use it, whether it
mints new ones, whether this server holds a key for it, and whether the
recovery kit is on. Counts only: no key material is shown.
backups recovery-kit on|off
PUT /api/v1/admin/instance/backups/settings/recovery-kit. With the kit on,
every key version the database references rides inside new instance
bundles (never workspace or crew bundles), so crewship recover can give the
restored server what it needs to read credentials. Whoever holds a private
backup key can then read every secret in the instance. Written to the instance
audit log.
backups check <path>
POST /api/v1/admin/instance/backups/bundles/check. Proof level 2 — contents
checked: the server decrypts the bundle, reads every section to the end and
compares table row counts and file digests with the manifest. Nothing is
restored. The key is sent for this request only and never stored. Exits
non-zero when the check fails.
backups restore-checks <path>
POST /api/v1/admin/instance/backups/restore/checks. What a restore checks
before anything changes: room on the data directory, whether the bundle’s
format is read directly or through a converter, whether Docker answers (a crew
built on another architecture is rebuilt from its image; crews built on an
older image than this server holds are named, with an images line counting
them), host settings that
are not carried over for safety (Docker socket mounts, privileged mode, host
path binds — read only with a key), and conflicts with the target. For a
bundle with complete container environments it prints one line per
environment: restore, rebuild (another architecture, or layers not
available here: the data comes back, the environment is rebuilt from the crew
image) or skip (no Docker), and the layers count toward the space check.
backups drills
GET /api/v1/admin/instance/backups/drills. Every test restore recorded with
crewship backup drill --post, newest first, with its result and how many
checks did not pass.
backups environments land
POST /api/v1/admin/instance/backups/environments/land. Loads the complete
container environments crewship recover staged under
<data-dir>/restore-staging/environments/, once the recovered server runs
with Docker. Each image is loaded back — a Crewship crew’s under its crew
image tag, so the crew is recreated from the captured environment — and a
container Crewship does not manage is recreated with its sanitized settings.
Every environment is reported restored, rebuilt or skipped with the
reason, and the settings not carried over for safety (Docker socket,
privileged mode, host paths, added capabilities, devices) stay off. Processes
start fresh. Written to the instance audit log. See
Complete container environments.
backups upload
POST /api/v1/admin/instance/backups/bundles/upload. Streams an encrypted local
archive, up to 64 GiB, with an instance-admin token and no selected workspace.
No private key is sent. The returned path is catalogued with checksum proof;
contents checks and a test restore are separate. The command sets no overall
timeout, so a slow link can take as long as it needs; Ctrl+C cancels the request.
Legacy archives needing conversion are marked explicitly.
backups services land
POST /api/v1/admin/instance/backups/services/land. Verifies the quota-service
disk images staged by whole-instance recovery, then imports them through the
new host’s configured quota helper. Generations are fresh, source namespaces
are provenance only, and maintenance release commits with the instance audit.
Failed imports or audits retain maintenance; do not resume these services by
bypassing the landing. The CLI waits up to 35 minutes; the server limit is 30.
backups settings get
GET /api/v1/admin/instance/backups/settings. The limits (runs at once, CPU
cores for compression, disk and upload MB/s), the heartbeat URL and its last
ping, which incidents reach the instance admins and after how many hours a
plan counts as stale, the drill reminder, the recovery kit, and where copies
are kept. “Also tell” lists the notification channels on the alert route with
the outcome of the newest backup alert each was sent; “Could tell” lists the
channels that could be added, with their ids.
backups test-alert
POST /api/v1/admin/instance/backups/settings/test-alert. Sends a test alert
to one notification channel now, through the same delivery a real backup
alert uses, and says whether it arrived. Exits non-zero when it did not. The
channel need not be on the alert route yet.
backups settings set
PUT /api/v1/admin/instance/backups/settings. Only the flags you pass change.
A run that would leave less than 10 % of the disk free does not start and
says why; the limits below decide how hard a running backup may push the
server. Written to the instance audit log.
backups recipients list
GET /api/v1/admin/instance/backups/recipients. The backup keys: the age
public keys backups are encrypted to, who holds each private half, its
fingerprint, and the plans that use it. The private halves never reach the
server. backups keys is the same command.
backups recipients add
POST /api/v1/admin/instance/backups/recipients. Adds a public key (age1…).
Name its id in a plan’s --recipient to encrypt to it. A private key
(AGE-SECRET-KEY-1…) is refused. Keep at least two keys held by different
people, so one lost key is not a lost backup. Written to the instance audit
log.
backups recipients remove <id>
DELETE /api/v1/admin/instance/backups/recipients/{id}. Refused while a plan
still encrypts to the key; change the plan first. Backups already made stay
readable with the key’s private half. Written to the instance audit log.
backups destinations list
GET /api/v1/admin/instance/backups/destinations. Off-site destinations,
how many bundles have a verified copy in each, the last connection test and
the plans that copy there. The secret access key is never shown.
backups destinations add
POST /api/v1/admin/instance/backups/destinations. Adds an S3-compatible
store (Cloudflare R2, Backblaze B2, Wasabi, MinIO, AWS). The server tests the
connection first — it puts, reads back and deletes one small object — and
stores nothing when that fails. The secret access key is read from a file,
sealed with the server’s vault key and never shown again. A private or
loopback endpoint (a MinIO on the LAN) and plain http need
--allow-private-network, and the answer then carries a warning; cloud
metadata and link-local addresses stay blocked either way. Name the
destination’s id in a plan’s --destination to copy that plan’s backups
there: each bundle is uploaded after it is written and counts as an off-site
copy only once the upload is verified at the destination. Written to the
instance audit log.
backups destinations test <id>
POST /api/v1/admin/instance/backups/destinations/{id}/test. Tests the
connection and permissions again and stores the outcome. Exits non-zero when
the test fails.
backups destinations remove <id>
DELETE /api/v1/admin/instance/backups/destinations/{id}. Refused while a
plan still copies there. Copies already uploaded stay in the bucket; only the
server’s record of them goes. Written to the instance audit log.
backups incidents
GET /api/v1/admin/instance/backups/incidents. One incident per plan and kind
— failed (a run failed, was skipped or was interrupted twice), incomplete,
stale, offsite, drill — counted on each repeat and resolved by the next
good run or when the condition clears. The drill reminder is one more
drill incident with no plan (plan_id null): no test restore within the
drill_reminder period, resolved by the next recorded drill. Open incidents
first. Each also sits in every instance admin’s inbox.
backups recovery-sheet
GET /api/v1/admin/instance/backups/recovery-sheet. A Markdown page to keep
off this server: where the bundles are (this server and every off-site
destination), the backup keys’ names and fingerprints, whether the vault keys
ride in the recovery kit, the exact crewship recover and
crewship backup drill commands, and who the instance admins are. It holds no
secret.
backups copies list
GET /api/v1/admin/instance/backups/copies. Every bundle an off-site
destination holds (not its environment layers), with scope, size and date, and
where this server already has it. Destination ids come from
backups destinations list.
backups copies fetch
POST /api/v1/admin/instance/backups/copies/fetch. Restore from an off-site
copy: downloads the bundle and the environment layers it needs into this
server’s backups directory, each verified, so it can be restored or drilled
like a local bundle. The server runs it as a job and answers at once — a large
download is never cut by the CLI’s request timeout. --wait follows the job
and prints where the bundle landed and the crewship recover command for it.
A bundle already on this server is refused with its path.
backups copies status <fetch-id>
GET /api/v1/admin/instance/backups/copies/fetch/{id}. A fetch started
without --wait: running, done (and where the bundle is) or failed. A server
restart forgets its fetches.
crewship admin instance holds
After crewship recover the server boots with routines and schedules, inbound
webhooks and queued work held. Check the restored server, then resume each.
holds list
GET /api/v1/admin/instance/holds. What is held, since when, and how much.
holds resume <key>
POST /api/v1/admin/instance/holds/resume. Resumes routines, webhooks,
queue or all. Written to the instance audit log.
crewship admin gdpr
Answers the two requests a person can make about their own data.
--reason is not paperwork. It is the audit trail, and it is what answers “why
was this person’s data removed” a year later — so delete refuses without it,
and refuses again without --yes. Both commands write an append-only row and
return an action id: that id is the receipt for the request you were
answering, so record it against the request.
export writes to stdout by default (pipe it, redirect it); --out writes a
file with 0600, since the file is every row about a person.
delete prints a per-table breakdown under the row total, and the labels carry
the verb, because the erasure does two different things:
/p/{token} link, an inbound webhook
token — stops working immediately. Only the second kind is counted in the row
total. inbox_item_reads is the subject’s own read markers on this workspace’s
inbox items — activity, not items about them. What the erasure promises, and the
accountability records it deliberately leaves behind, are in GDPR.
An erasure that only partly happened — one table refused or a disk copy could
not be removed — comes back as 207 Multi-Status. The receipt is still printed,
but the command exits non-zero with erasure incomplete: <first error> and
the audit action id, so a scripted SAR queue cannot record it as done; re-run
after fixing the cause (the cascade is idempotent, a second run reports zero for
what already went).
crewship admin security-posture
Report how this instance is postured: encryption at rest, the private-egress ceiling, signup policy, rate limiting, and whether email/OAuth are configured.
No secret value is ever returned — only whether something is configured. That is the whole contract, and it’s what lets this be a plain admin
GET with nothing worth stealing in the response. A regression test asserts no secret-shaped env value reaches the wire, including via a warning message.What it reports
Each value is read through the same accessor the runtime enforces with, not re-parsed locally. A posture that parses a flag its own way eventually disagrees with the code that acts on it, and a security report that confidently states the wrong thing is worse than no report.
Intent vs. effect
The rate limiter is reported twice on purpose. In production the limiter runs regardless of the disable flag (MustNotDisableRateLimitInProd), so:
Warnings
Beyond the raw state, the report derives warnings that name the consequence:plaintext_secrets_allowed(high) — anyone with the database file can read every stored secret.encryption_key_missing(medium, high when combined with the above) — the combination means new credentials are being written in the clear.rate_limit_disabled(medium) — credential-stuffing and the/credentials/testoracle are unthrottled.rate_limit_disabled_ignored_in_prod(info) — a disable flag is set but the production guard keeps the limiter running. Not an exposure; it means your config and the running state disagree, which matters the first time that config lands somewhere the guard doesn’t apply.signup_open(medium, high in production) — anyone who can reach the instance can create an account.private_endpoints_ceiling_open(info) — crews that also opt in can reach RFC1918/loopback. Link-local and cloud-metadata stay blocked regardless.
encryption_key_generated(medium) — Crewship bootstrapped the master key itself instead of an operator supplying one, so the key lives with the data it protects.privileged_credentials_enabled(high) — at least one workspace hasallow_privileged_credentialson, which removes the fail-closed boundary between privileged crews and stored secrets.private_endpoints_in_use(medium) — crews have opted into private-endpoint egress and the instance ceiling is open, so the reach is live rather than merely permitted.seed_account_default_password(high) — thecrewship seeddemo account still authenticates with the password printed in these docs.no_backup_recorded(medium) — nobackup.*action appears in the audit trail. Reported only when that query succeeds: a failed probe is unknown state, not a missing backup.
crewship admin memory-config
Read or adjust workspaces.memory_config — currently the retention window for memory_versions rows, consumed by the per-workspace retention sweep.
The command also accepts the alias crewship admin memcfg.
GET/PATCH /api/v1/admin/memory/config) exists so retention can be changed without editing SQLite by hand. Until this command there was no client for it, so the hand-edit stayed the only route — which is what the endpoint was built to avoid.
memory-config get
Prints the resolved value and, importantly, whether it was chosen:
30 days on its own doesn’t tell you whether anyone picked it. That matters before changing it: overriding a built-in default is routine, overriding somebody’s deliberate policy is not.
When the stored document carries keys this CLI version doesn’t model, they are printed verbatim underneath rather than hidden — otherwise get would look authoritative when it isn’t.
memory-config set --retention-days N
Writes are manage-tier and emit
memory.config_updated to the journal, so a compliance audit can trace when retention policy changed and who changed it.
Sent as a partial PATCH carrying only the key being changed. The server merges it into the stored document and preserves settings this CLI doesn’t know about, so an older binary can’t silently drop a newer knob by writing a whole document back.
The same data backs the Admin → Runtime → Memory configuration card in the dashboard.
crewship admin memory-stats
Companion read to memory-config: memory-config adjusts the retention
window, memory-stats shows what is actually being retained under it.
GET /api/v1/admin/memory/stats, scoped to the current workspace. Three
figures:
- Totals — version count, byte sum, and distinct blob count (a stale row
and a fresh row at the same content share one blob), plus the oldest/newest
written_atin the window. - By tier — one row per memory tier (
agent,crew,workspace, …); tiers with zero rows are omitted. - By agent — one row per agent slug that has written memory. Crew- and
workspace-tier rows have no agent slug and are collapsed under
(shared: crew/workspace tier).
OWNER or ADMIN (canRole "manage").
crewship admin memory sync
Run one of the two daily memory sweeps now instead of waiting for its
scheduled tick, and see the per-workspace summary the sweep otherwise only
logs. Two subcommands, one per sweep:
curator slot cannot be built, or
when the person is below the interaction threshold — and all four look the
same from outside. The summary tells them apart:
A
TOTAL row is added when more than one workspace was swept.
Scope defaults to the current workspace (-w / config). --all runs every
active workspace on the instance — exactly what the scheduler does at its
tick. The sweeps are safe to re-run: the merge is idempotent and the write is
capped.
--dry-run runs everything up to the write — including the extractor, which
is the model-backed step worth observing — and reports what would have
happened. Nothing lands on disk, in the index tables, or in the audit log.
This is how a measurement like “how many facts does the extractor propose,
and how many does the gate refuse” becomes repeatable without planting data
in a live instance; the per-extraction counts are on the server log’s
user model extraction lines.
The peer-card sweep has no extractor wired on the server today, so it
indexes and purges without writing new bodies — the same thing the 04:00
sweep does — and every threshold-crosser shows under EMPTY.
--format json prints the server’s report verbatim (sweep, dry_run,
workspaces[], totals, duration_ms).
Backed by POST /api/v1/admin/memory/user-model-sync and
POST /api/v1/admin/memory/peer-card-sync. Requires OWNER or ADMIN
(canRole "manage").
crewship admin ratelimits
Inspect and tune the instance’s rate limiters at runtime — the same surface as the dashboard’s Infrastructure → Rate Limiters tab. Every limiter that was once a hardcoded constant (the per-IP HTTP buckets, the login lockout, the notification anti-storm bucket, crew provisioning, agent webhooks) is listed and overridable without a redeploy.
Aliases: crewship admin rate-limits and crewship admin ratelimit.
list shows each limiter’s group, key, current value, shipped default, unit, and whether the value is a default or an override:
set <key> <value> overrides a limiter; reset <key> reverts it to the shipped default. Overrides persist in the instance-global rate_limit_overrides table and take effect immediately — the per-IP HTTP buckets are retuned on the live limiter, the rest read their value on next use.
Writes are manage-tier (OWNER/ADMIN) and logged at WARN with the actor. A value outside a limiter’s allowed range is rejected (
400); an unknown key is 404.
Raising
http.auth_per_min is the fix for an instance whose users get logged out under heavy refresh behind a shared NAT. On current builds the read-only session polls that caused that no longer touch this bucket, so most instances never need to.crewship admin seed-inbox
Fill a workspace’s inbox with one row of every kind — waitpoints, escalations, a failed run, messages, a memory proposal, schedule alerts — plus approval-queue rows both pending and decided. It exists because the inbox has no create endpoint: every row is written by a producer (a pipeline reaching a waitpoint, an agent escalating, the keeper sweeping), so a fresh workspace shows an empty inbox and there is no way to see how the views, facets and the reading pane behave against real variety.
Rows go in through the same writers the real producers use, so what lands is shaped exactly like production data rather than hand-built SQL that agrees with whatever the UI happens to expect.
crewship admin cleanup
Read container cleanup observations from the server selected by the CLI. This HTTP read requires login. Instance admins can omit the workspace and read the whole installation; workspace ADMIN/OWNER users must select a workspace and see only its observations. Unlike local recovery commands, it does not open a database file. It is backed byGET /api/v1/admin/resource-cleanup and never triggers cleanup itself.
scope=containers, controller state and per-crew items
for this installation (instance admins) or the selected workspace (workspace
admins), with observation time, completeness, remaining eligible containers (and
per-crew networks, when the crew had one), legacy unattributed count and fixed
error codes. observed_clear describes the last complete eligible-container and
crew-network scan, not volumes, host data or future creates.
Stale observations read as unknown.
If the crew’s workspace is deleted or the server is unavailable, use
crewship doctor cleanup --format json on the host to read historical local
snapshots without workspace membership.