Skip to content

Guide: Run an AI agent safely ​

Scenario: You want Claude, Gemini CLI, or a cron script to query your finances unattended — with a hard guarantee it cannot move money or mutate data, without your Monarch password sitting on disk, and with outputs an automation can actually parse.

This guide is the operator's checklist. For the command-by-command view see the Agent Integration Guide; for the underlying model see the Safety Model.

All output blocks in the guides were captured from real runs of monarch and then anonymized: IDs, names, and amounts are replaced, but the output shape is exactly what the tool emits.

Step 1: take writes off the table ​

Set read-only mode in the environment you hand to the agent:

bash
export MONARCH_READONLY=1

Now every mutation — even with --confirm — is rejected before any network call:

console
$ MONARCH_READONLY=1 monarch transactions delete 331288001122334455 --confirm
{"ok":false,"error":{"code":"READ_ONLY_VIOLATION","message":"remote writes are blocked in read-only mode","category":"safety","retryable":false},"meta":{"command":"transactions.delete","profile":"default","duration_ms":0,"schema_version":"2026-05-08","request_id":"d9b7d82f-10aa-4b5e-bed7-ad48d1fcc98b"}}
$ echo $?
4

duration_ms: 0 is the tell: the gate fires before authentication and before any API request. The equivalent flag (--read-only) exists for one-off use; prefer the env var for agents since it survives arbitrary subcommands.

Step 2: keep the secret out of the session file ​

Normal auth login stores a long-lived token in ~/.monarchmoney-cli/session.json. For unattended machines, store an indirection instead: write a 0600 session file whose token field names an environment variable.

json
{
  "profile": "default",
  "email": "you@example.com",
  "created_at": "2026-01-15T08:00:00Z",
  "updated_at": "2026-01-15T08:00:00Z",
  "token": "env:MONARCH_TOKEN"
}

Each run resolves MONARCH_TOKEN from the environment — supplied by your shell profile, a secret manager, or CI secrets. The real secret never sits in a file; if the variable is unset the CLI fails with an explicit auth error rather than proceeding unauthenticated. Rationale in ADR-0006.

For interactive logins from scripts, the same idea applies to credentials: pass MONARCH_EMAIL, MONARCH_PASSWORD, and MONARCH_MFA_SECRET as environment variables, never flags (flags leak into the process table).

Step 3: give the agent a parseable contract ​

Always run commands with --json; then stdout carries exactly one envelope and stderr carries diagnostics only:

console
$ monarch transactions list --limit 1 --json
{"ok":true,"data":[{"id":"331288001122334455","date":"2026-07-12","amount":-6.75,"merchant":"Maple Roast Coffee","category":"Shopping","category_group":{"id":"221000445566778899","name":"Food & Dining","type":"expense"},"notes":"","tags":[],"goal":{"id":"","name":""},"pending":false,"hide_from_reports":false,"plaid_name":"MAPLE ROAST #212","data_provider_description":"MAPLE ROAST COFFEE TORONTO","is_recurring":false,"review_status":"needs_review","needs_review":true,"is_split_transaction":false,"created_at":"2026-07-14T21:46:18.576612+00:00","updated_at":"2026-07-29T00:35:11.006715+00:00","account_id":"221000112233445566","account_order":23,"account_type_group":"liability","owner_display_name":""}],"meta":{"command":"transactions.list","profile":"default","duration_ms":301,"schema_version":"2026-05-08","request_id":"c1a4e9d0-..."}}

The agent's decision loop should branch on two things: the process exit code, and ok.

ExitMeaningAgent action
0SuccessParse data from stdout
1Internal / not foundReport; do not retry
2Invalid argumentsFix arguments and retry
3AuthAsk the human to re-authenticate
4Read-onlyExplain the operation is blocked by policy
5Network / rate limitRetry with backoff
6API errorSurface the message; do not retry blindly
7ValidationCorrect the input values
10Confirmation requiredPresent the plan, ask the human about --confirm

Exit 10 is the designed human-in-the-loop hook: when an agent proposes a mutation, it runs with --dry-run, shows the returned mutation plan, and only escalates to --confirm --json after explicit approval (see Transactions workflow). Full envelope details live in JSON_SCHEMA.md.

Step 4: know what works offline ​

Reads against the local cache need no session and no network — ideal for flaky schedulers or air-gapped review:

console
$ monarch cache stats
Cache Statistics
holdings: 3
last_synced_at: 2026-08-21T05:43:15Z
accounts: 69
transactions: 3394

$ monarch cache search "Maple Roast" --json

And the full plain-text export (Ledger backup) is a pure cache-to-file function:

console
$ monarch hledger backup /tmp/snapshot.journal --json
{"ok":true,"data":{"accounts":69,"file":"/tmp/snapshot.journal","holdings":3,"status":"backup complete","transactions":3394},"meta":{"command":"hledger.backup","profile":"default","duration_ms":194,"schema_version":"2026-05-08","request_id":"d58b98f7-51ff-4203-8398-819209cc9482"}}

Schedule monarch cache sync separately (it is the networked step) and keep agents pointed at the offline commands.

Step 5: watch long-running refreshes ​

accounts refresh --wait --events emits NDJSON progress envelopes so an orchestrator can show or log progress:

json
{"ok":true,"data":{"is_complete":false,"status":"syncing","accounts":[{"id":"221000112233445566","has_sync_in_progress":true}]},"meta":{"command":"accounts.refresh.progress"}}
{"ok":true,"data":{"is_complete":true,"status":"complete","accounts":[]},"meta":{"command":"accounts.refresh.progress"}}
{"ok":true,"data":{"status":"refresh complete"},"meta":{"command":"accounts.refresh"}}

Step 6: leave an audit trail ​

Every executed mutation appends to ~/.monarchmoney-cli/audit/YYYY-MM-DD.jsonl:

json
{
  "timestamp": "2026-08-01T22:12:00Z",
  "command": "transactions.update",
  "resource_id": "331288001122334455",
  "dry_run": false,
  "confirmed": true,
  "profile": "default",
  "result": "success",
  "error_code": ""
}

Records never contain credentials or financial details. Prune old files periodically: monarch audit cleanup (default 30 days, customize with --older-than N). In read-only mode there is nothing to audit — which is the point of step 1.

Next steps ​

Built with VitePress