Global flags
These flags are persistent, so every command accepts them:
| Flag | Type | Description |
|---|---|---|
--analysis-json | string | versioned analysis JSON patch; protected settings may only narrow (SONNY_ANALYSIS_JSON supplies the environment layer) |
--config | string | config file (default: .sonny.yaml in working directory) |
--debug | — | enable debug mode |
-h, --help | — | help for sonny |
--log-format | string | log format: text|json (default “text”) |
--log-level | string | log level: debug|info|warn|error (default “warn”) |
-m, --model | string | model to use (overrides recipe default) |
-p, --provider | string | LLM provider (openai, anthropic, ollama, etc.) |
--quiet-deprecations | — | suppress schema and legacy recipe-name deprecation warnings |
-v, --version | — | version for sonny |
-C, --workdir | string | working directory (default “.”) |
Bare sonny is sonny run
Bare sonny with no subcommand is an alias for sonny run
.
The legacy shim in main.go rewrites sonny --dir foo into sonny run --dir foo
before cobra ever sees it, and also promotes old single-dash long flags (-dir)
to double-dash (--dir).
Two things follow from that shim:
sonny -vdoes not work. Cobra’s root-vis a real flag (--version), but the shim only rewrites args that start with-intosonny run-style invocations, andrunhas no-vshorthand, so it fails as an unknown shorthand flag (exit2). Use--version(two dashes) instead.--license,--sbom,--changelog, and--third-party-licensesonly exist onsonny version— there is no root-level equivalent.
Exit codes
Every classified refusal leaves the process with the status its code owns in
this table. Exit 1 means only “an error escaped classification” — a defect
worth reporting, never a reasoned refusal. A malformed invocation (unknown flag,
unknown command, wrong argument count) is always 2.
| Status | Code | Meaning |
|---|---|---|
| 0 | ok | succeeded |
| 1 | — | an unclassified error escaped; not a published outcome |
| 2 | usage | malformed invocation: unknown flag, unknown command, bad argument count |
| 3 | config | configuration missing, unreadable, or contradictory |
| 4 | incompatible_protocol | no compatible wire version was negotiated |
| 5 | unauthenticated | no usable identity |
| 6 | unauthorized | an identity exists but lacks the authority |
| 7 | conflict | expected revision, lease, or aggregate was won by someone else |
| 8 | denied_policy | a policy rule refused the operation |
| 9 | denied_evidence | required evidence is missing, stale, or failed verification |
| 10 | denied_approval | a required human approval is absent or expired |
| 11 | frozen | a freeze is in force (suspended, not forbidden) |
| 12 | unavailable | a dependency is down or unreachable |
| 13 | timeout | the operation did not finish in its budget |
| 14 | partial | some of the requested work applied and some did not |
| 15 | indeterminate | sonny cannot establish whether the effect happened |
| 70 | internal | a defect in sonny |
| 71 | corruption | persisted state failed an integrity check |
Under --json the same code rides inside the envelope’s error.code /
error.exit_code, so a consumer never has to branch on prose. Some commands
still predate the table and exit 1 or 2 on their own
(sonny run --session <unknown-uuid> exits 2); those are being migrated
command by command.