CLI

CLI guide

Every diagnos CLI command with real examples: login, patients, exams, files, sessions, JSON output for scripts and cron jobs.

On this page

English · Português (Brasil)

diagnos is the SDK in your terminal: the same enrollment, the same encryption, the same errors, with human-readable tables by default and JSON for scripts. Every command is listed with all its options in the CLI reference; this guide is about using them together.

diagnos --help                  # every command
diagnos patients create --help  # one command, with examples at the bottom

Global options#

Global options go before the subcommand — diagnos --json patients list, not diagnos patients list --json.

Option What it does
--json machine-readable output: plain JSON on stdout, no tables, panels or spinners
--quiet, -q no spinners or progress bars
--token TOKEN overrides DIAGNOS_API_TOKEN for this invocation; never echoed anywhere
--vault-url URL overrides DIAGNOS_VAULT_URL for this invocation
--no-color no ANSI colors (so does the NO_COLOR environment variable)
--version prints the CLI version and exits

Everything else — timeouts, time precision, OpenBao — comes from the environment, exactly as for the SDK (Configuration).

Enrollment in the terminal#

Each diagnos invocation is its own process, and a process needs an approved session before it can decrypt anything. The first command that needs one shows a panel on stderr and waits:

╭────────────────────── diagnos · enrollment ───────────────────────╮
│ Open this link and type the code below to approve this session.   │
│ Abra este link e digite o código abaixo para aprovar esta sessão. │
│                                                                   │
│ https://…                                                         │
│                                                                   │
│   4  8  2  9  1  5                                                │
╰───────────────────────────────────────────────────────────────────╯
⠋ waiting for approval… · aguardando aprovação…

An admin opens the link, checks the machine described there, types the code and picks the groups. login does only this, and then shows what was granted:

diagnos login
diagnos --json login | jq -r '.security_groups[]'
Enrolled · Sessão estabelecida
  workspace_id: ws_...
  account_id:   acct_...
  groups · grupos: sg_oncology, sg_radiology

status parses the token locally and reports OpenBao and the memory enclave without touching the network; status --check also unlocks and lists the granted groups, and groups lists them one per line.

diagnos status
diagnos status --check
diagnos groups

Not approving every command#

Without OpenBao, the session dies with the process: the next invocation enrolls again, with a new link and code. That is the right default on a laptop — login is for checking a token and seeing what it grants. For anything that runs unattended, configure OpenBao auto-unseal: the first invocation enrolls once and saves the session, and every later one restores it silently until it expires.

export OPENBAO_ADDR="https://openbao.internal:8200"
export OPENBAO_TOKEN_FILE=/run/secrets/openbao-token
diagnos login                    # a person approves once; the session is saved to OpenBao
diagnos --quiet patients list    # restored — no prompt
diagnos login --no-auto-unseal   # enroll without saving, even with OPENBAO_ADDR set

diagnos session lock ends the session: it wipes the saved copy in OpenBao, so the next invocation enrolls again.

Patients#

Lists are anonymous by default — ids, versions, groups and flags — so a list never prints names you did not ask for. --summary decrypts each row's names and tags; get prints the whole record.

diagnos patients list --group sg_oncology            # one page, anonymous
diagnos patients list --group sg_oncology --summary  # names and tags too
diagnos patients list --all                          # every page
diagnos patients list --limit 20 --cursor CURSOR     # the next page; the cursor is printed under each page
diagnos patients list --include-deleted              # the trash too
diagnos patients get PATIENT_ID                      # the newest content: a newer web-editor draft wins
diagnos patients get PATIENT_ID --committed          # saved versions only
diagnos patients get PATIENT_ID --version VERSION_ID # one exact version

Create from inline flags, or from a JSON file with the complete record:

diagnos patients create --group sg_oncology --legal-name "Jane Doe" --display-name Jane \
  --birth-date 1990-01-31 --external-id MRN-0042 --tag diabetes --tag follow-up
diagnos patients create --group sg_oncology --file patient.json
{
  "legal_name": "Jane Doe",
  "display_name": "Jane",
  "birth_date": "1990-01-31",
  "biological_sex": "FEMALE",
  "address": { "city": "São Paulo", "state": "SP", "country": "BR" }
}

An update writes a complete new version from a file. --expect-version makes the vault refuse it if someone saved in between (exit code 7); --tag replaces the tags, and omitting it keeps them:

diagnos --json patients get PATIENT_ID | jq '.record' > patient.json   # read
$EDITOR patient.json                                                    # change
diagnos patients update PATIENT_ID --file patient.json --expect-version VERSION_ID   # write

Archive and delete are flags; delete asks for confirmation unless --yes, and is never a hard delete:

diagnos patients archive PATIENT_ID
diagnos patients unarchive PATIENT_ID
diagnos patients delete PATIENT_ID --yes
diagnos patients restore PATIENT_ID

The record's fields, dates and typo protection are the SDK's — see Patients.

Exams#

The same verbs, with two differences: create needs the patient, the one field that travels in clear, and get prints the report as plain text (--json has the full record, HTML included).

diagnos exams list --group sg_radiology --summary
diagnos exams create --patient PATIENT_ID --group sg_radiology \
  --title "Chest CT" --modality CT --exam-date 2026-09-01
diagnos exams create --patient PATIENT_ID --group sg_radiology --file exam.json
diagnos exams get EXAM_ID
diagnos exams update EXAM_ID --file exam.json --expect-version VERSION_ID
diagnos exams delete EXAM_ID --yes

Write both report_lexical and report_html in exam.json when people will open the report in the web editor — Exams explains why.

Files#

diagnos files upload --group sg_oncology --exam EXAM_ID scans/*.dcm   # many at once, 100 per reservation
FOLDER=$(diagnos --json files mkdir "CT 2026-09-01" --group sg_oncology | jq -r .node_id)
diagnos files upload --group sg_oncology --folder "$FOLDER" report.pdf
diagnos files list --group sg_oncology --folder "$FOLDER"             # names decrypted
diagnos files list --exam EXAM_ID --all
diagnos files list --include-pending                                  # unfinished uploads too
diagnos files get NODE_ID                                             # metadata and name, never content
diagnos files download NODE_ID                                        # to ./<its name>
diagnos files download NODE_ID -o scan.dcm

download without -o writes to the file's decrypted name in the current directory — its last path segment only, because the web app seals relative paths as names and a crafted ../../.bashrc must not escape. Reading a file needs only its node id; --group on list is a filter, not a requirement.

Scripting#

JSON output#

With --json, stdout is exactly one JSON document — never wrapped or colored by the terminal renderer, so a long field cannot break a line. The shapes match the REST API's:

Command stdout
patients list, exams list {"items": [{"index": …, "summary": null or …}], "next_cursor": …}
patients get, exams get, create, update the whole Patient / Exam: index, record, summary, version_id, draft_rev
archive, unarchive, delete, restore the updated index
files list, files upload {"items": [{"node": …, "name": …}], "next_cursor": …}
files get {"node": …, "name": …}
files mkdir {"node_id": …}
files download {"node_id": …, "saved_to": …}
login {"workspace_id": …, "account_id": …, "security_groups": […]}
groups {"security_groups": […]}
status workspace_id, account_id, openbao_configured, sdk_version, memory — and security_groups with --check
diagnos --json patients list --all | jq -r '.items[].index.document_id'
diagnos --json exams list --all | jq -r '.items[] | select(.index.meta.patient_id == "PATIENT_ID") | .index.document_id'
diagnos --json files list --exam EXAM_ID --all | jq -r '.items[] | "\(.node.node_id)\t\(.name)"'

Errors never go to stdout: they go to stderr as one line, and the exit code says what happened.

Exit codes#

0 is success; 2 a configuration problem; 3 authentication, permission, session or enrollment; 4 not found; 5 quota; 6 rate limited; 7 conflict; 1 anything else. The full mapping from each SDK exception is in Errors.

Unattended jobs#

#!/bin/sh
# nightly-upload.sh — run from cron, with OPENBAO_ADDR and OPENBAO_TOKEN_FILE in the environment
set -eu
diagnos --quiet --no-color files upload --group sg_radiology --exam "$EXAM_ID" /data/outbox/*.dcm

Use --quiet so no spinner writes to the log, OpenBao so no run waits for a person, and the exit code — not the text — to decide what happened.