On this page
#Changelog
#0.39.0
Consequential commands now require explicit consent on the programmatic and MCP channels, tool descriptors publish their effects classification, and approve_consequential becomes a reserved parameter name.
Context
The confirmation requirement was a CLI-only property: a command marked consequential prompted (or refused) when it was reached through argv, but the same command was reachable with no consent at all through as_tools(), call(), acall(), Tool.execute and MCP tools/call. An agent driving the app over MCP therefore ran exactly the commands the requirement exists to stop for a human decision. This release makes consent a property of the command rather than of the channel: every call path refuses a consequential command unless the caller states consent, and the descriptors published to tool and MCP consumers carry effect and consequential so a caller can see the requirement before it calls.
Reserving approve_consequential follows from that. The name is now framework vocabulary in all three implementations, so a command declaring its own flag or positional arg of that name would shadow the consent parameter -- and in Python a positional arg of that name was already unreachable over call() while the same command stayed callable over MCP. Registration now rejects it outright.
Both changes are breaking, which under pre-1.0 versioning is a minor bump. The three implementations ship the same behavior in lockstep, verified by the conformance suite.
#Breaking
- [strictcli] Tool export and programmatic calls honour the confirmation requirement.
as_tools()descriptors and MCPtools/listnow publisheffectandconsequentialbeside the argument schema, so a caller can see which tools require confirmation.call(),acall(),Tool.executeand MCPtools/callrefuse aconsequentialcommand unless the call states consent (approve_consequential=True, or theapprove_consequentialparam on an MCPtools/call). - [strictcli] **
approve_consequentialis a reserved parameter name.** Declaring aFlagor anArgnamedapprove_consequentialis now a registration-timeValueError. The name is how a caller states consent to aconsequentialcommand throughcall(),acall(),Tool.executeand MCPtools/call; a positional arg of that name was unreachable overcall()(the keyword-only consent parameter swallowed it) while the same command stayed callable over MCP.
#Features
- [strictcli] The built-in
configsubcommands (path, show, set, edit, init) now explain what they actually do — precedence order, type coercion, comma-escaping and dry-run behavior — instead of one terse line.
#0.38.0
Receiver-aware effects-bypass lint: platform.system() is no longer a false positive, and aliased or from-imported receivers are now caught.
Context
The effects-bypass check matched bare leaf names, so any call named system was reported as an escaped effect -- including platform.system(), a pure in-process read whose suggested remediation could not be followed. The leaf is now a finding only through the os receiver (os.system, import os as o, from os import system).
Resolution runs through the module's own imports, which also closes the opposite gap: import requests as rq + rq.post(...) and from subprocess import run + run(...) were previously missed and are now caught. Builtin open is answered before import resolution so it keeps its own verdict.
Consumers pick the fix up on their next lock bump.
#Fixes
- [strictcli] **
effects-bypassno longer flagsplatform.system().** Thesystemleaf is now a finding only through theosreceiver (os.system,import os as o,from os import system), so a pure in-process read no longer produces a finding whose remediation could not be followed. Receivers are also resolved through the module's imports, soimport requests as rq+rq.post(...)andfrom subprocess import run+run(...)are now caught.
#0.37.0
Adds the dry_run_supported=False command declaration -- a mutating command can now refuse --dry-run with a mandatory reason carried into help and the schema -- and corrects a Python README that documented a handler signature, a check API and a dependency claim the framework no longer has.
Context
--dry-run is a promise: the preview must describe what the real run would do. Some mutating commands cannot keep that promise, because their effects are not representable ahead of time. Until now such a command either rendered a preview that misrepresented it or hand-rolled a refusal inside the handler -- after parsing, invisible to --help and to --dump-schema. dry_run_supported moves the refusal to the registration site, where the framework can enforce it: the reason is mandatory, it is rendered in a dedicated Dry run: help section, it is emitted in the schema, and declaring it on a read_only command is a registration-time error (a command that changes nothing has nothing to preview).
The same release completes a documentation truth pass. Every registration example was re-checked against the shipped API, and the reserved flag quartet, the effects regime and the consequential confirm protocol -- previously explained nowhere -- are now documented. Examples are no longer trusted by inspection: the self-contained example programs are marked validate, and selfdoc check now assembles and RUNS each one against the checkout, so documentation that stops working fails the release rather than rotting quietly.
#Features
- [strictcli] **
dry_run_supported=Falsecommand declaration.** A mutating command whose effects a preview cannot honestly represent can now declaredry_run_supported=Falsewith a mandatorydry_run_unsupported_reason.--dry-runis then refused at parse time with the reason instead of rendering a preview that would misrepresent the real run; the reason also appears in a newDry run:help section and in the schema. Declaring it on aread_onlycommand is a registration-time error.
#Fixes
- [strictcli] The Python README now matches the shipped API. It documented the pre-
ctxhandler signature, omitted the mandatoryeffect=classification from every example, used flag names the framework reserves (verbose,quiet), claimed zero dependencies despite requiring tomlkit, claimed bool flags default toFalsewhen they are required without an explicit default, and described the removed@app.check/CheckResultAPI. It now also documents the effects regime, the reserved flag quartet,dry_run_supportedandconsequential.
#0.36.0
Confirmation keys on a declared consequential, not on mutating
Context
The shipped regime INFERRED "mutating => must confirm". Adoption across six consumers falsified that inference by an order of magnitude: 391 of 624 commands (63%) classify mutating, so two thirds of every CLI in the fleet prompted -- including safegit commit, rlsbl changelog add, selfdoc gen, saferm delete (the safe, undoable deletion) and claudewheel launch, which had to be special-cased or every session would have opened with a Proceed? [y/N]. The genuinely dangerous commands are roughly 5-10% of that set: a ~1:10 signal-to-noise ratio, which guarantees the skip flag gets passed reflexively and leaves the guardrail dead while it still looks present.
The diagnosis: one field was answering two questions. effect correctly answers "should a dry run record rather than execute?" -- that is working and is unchanged. Consequence asks "are these effects worth interrupting someone for?" Almost everything has effects; very little is consequential. effect was only ever a proxy for the second question, and the proxy is off by 10x.
Commands now declare consequential=True and the framework prompts for those and no others. The declaration is deliberately named after a property of the COMMAND rather than after the framework's reaction, so other behaviours can hang off the same fact later. The skip flag --approve-consequential is deliberately unwieldy: a flag that cannot become muscle memory stays a decision. yes stays on the banned-names list so nobody reintroduces a private --yes meaning the same thing.
#Breaking
- [strictcli] **Confirmation now keys on a declared
consequential, not onmutating.** Commands opt in withconsequential=Trueonapp.command(...); a plainmutatingcommand never prompts. The skip flag is--approve-consequential(--yesis gone, and the nameyesstays banned for command and global flags);ctx.yesbecomesctx.approve_consequential. Declaringconsequentialon aread_onlycommand is a registration-time error, and the newconsequential-grant-agreementcheck warns when a command declares a process- or network-mutating grant without declaring itself consequential.
#0.35.4
Dry-run previews render on every exit path out of a handler
Context
A consumer migrating onto the effects regime found that a dry-run preview vanished whenever the handler left through anything other than a normal return. The would-do log was rendered only on the success path, so a handler that recorded its intended writes and then exited non-zero -- the ordinary shape of a validation command -- printed nothing at all. Safety was never at risk (dry mode still executed nothing), but the preview property was: silence is indistinguishable from 'this command would do nothing', and those runs are exactly the ones a reader most wants to see. The render is now owned by the single seam every dispatch already passes through in each language, so it cannot be missed by a path that forgets to call it. An unexpected crash still renders what was recorded, followed by a new stderr marker saying the preview may be incomplete; the exception itself is never swallowed. A handler that terminates the process itself (Go os.Exit, Node process.exit) remains outside the guarantee and is recorded as a ceiling in the effects contract.
#Fixes
- [strictcli] Dry-run previews no longer vanish. A
mutatinghandler that recorded effects and then exited throughsys.exitor an uncaught exception printed nothing at all under--dry-run; the would-do log now renders on every exit path, and an unexpected crash marks the preview as possibly incomplete on stderr.
#0.35.3
Recognize the reserved flag quartet anywhere in argv
Context
The quartet (--dry-run, --yes, --quiet, --verbose) was extracted by a pre-scan that stopped at the first non-flag token, so it was only recognized BEFORE the command name -- while every documented invocation in this ecosystem writes these flags after it. myapp deploy --dry-run failed with unknown flag '--dry-run', and the first consumer to migrate onto the effects regime had to rewrite argv before handing it to the framework to make its own documented commands work.
The quartet is now recognized anywhere in argv, matching the framework's own existing precedent for --help/-h. Two boundaries are preserved: a bare -- (everything after it is positional data) and a passthrough command's name (its args belong to the child process and are forwarded byte-for-byte, so a child's own --verbose is never eaten). --hermetic, --config, --dump-schema and --mcp stay pre-command-only. Effects contract §7.2 is amended accordingly (adoption ruling A1, §18.6).
#Fixes
- [strictcli] The reserved flags now work after the command name.
--dry-run,--yes,--quietand--verboseare recognized anywhere in argv, exactly like--help--myapp deploy --dry-runused to fail withunknown flagand now works, at any group nesting depth. A bare--still ends recognition, and a passthrough command's args are still forwarded to the child untouched.
#0.35.2
Ship the 0.35.0 library content to PyPI, which neither 0.35.0 nor 0.35.1 reached.
Context
0.35.0 and 0.35.1 are both phantom versions: each has a git tag and a GitHub Release, and neither has an artifact on PyPI. This release ships that same library content -- unchanged since 0.35.0 -- to the registry.
0.35.0 failed because it was released as a three-releasable batch that pushed three separate candidate commits, and all three tags ended up on the final candidate. That commit changed nothing under python/, so the CI Router's paths filter skipped strictcli-ci, and the publish gate refused: a skipped check proves nothing about the commit. The batch defect is fixed upstream in rlsbl, which now commits every member and pushes exactly once.
0.35.1 failed differently. Its candidate was pushed and CI went green on it, but a commit from a concurrent session landed on the branch during the CI wait and the release aborted at its foreign-commit guard -- correctly. Resuming that release then tagged the branch tip instead of the CI-verified candidate it had recorded, so the tag pointed at a commit that had never produced any CI check runs at all, and the gate refused again. Both refusals were the gate working as designed; nothing was relaxed or bypassed.
This release is a clean single-releasable run from a quiet branch. Its version bump touches python/, so strictcli-ci runs on the tagged candidate and the gate can pass honestly.
There are no user-facing changes. 0.35.0's user-facing entries are already finalized into its own changelog, and the library code is byte-identical to what 0.35.0 and 0.35.1 would have shipped. This is infrastructure only: its sole purpose is getting that content to the registry.
#Infrastructure
- Ship the 0.35.0 library content to PyPI, which neither 0.35.0 nor 0.35.1 reached.
#0.35.1
Republish 0.35.0's library content to PyPI after the 0.35.0 tag was published but never reached the registry.
Context
0.35.0 was tagged and got a GitHub Release, but its artifact never reached PyPI.
The 0.35.0 release ran as a three-releasable batch. That batch pushed three separate candidate commits, and all three tags ended up pointing at the final candidate. On that final commit the CI Router's per-project paths filter saw no change under python/, so the strictcli-ci / test job was skipped. The Publish Router's gate treats a skipped CI check as a hard failure -- correctly, since a skipped check proves nothing about the commit -- and refused to publish. The gate behaved exactly as designed; the defect was upstream, in the batch push.
The root cause is fixed in rlsbl: the batch path now commits every member and pushes exactly once, so the gated commit is the commit all tags point at and every member's CI actually triggers.
Retrying 0.35.0 is not possible: re-running CI on that SHA reproduces the same skip, because the SHA genuinely contains no python/ change. This release therefore ships the identical library content under a new version whose release commit does touch python/, so strictcli-ci runs on the tagged candidate and the gate can pass honestly.
There are no user-facing changes here: 0.35.0's user-facing entries are already finalized into its own changelog. This is an infrastructure release whose sole purpose is getting that content to the registry.
0.35.0 remains a phantom version -- a tag and a GitHub Release with no corresponding PyPI artifact. It was never installable and never will be.
#Infrastructure
- Republish 0.35.0's library content to PyPI after the 0.35.0 tag was published but never reached the registry.
#0.35.0
The effects regime: mandatory command classification, framework-owned --dry-run/--yes/--quiet/--verbose, and side effects that flow through ctx.effects.
Context
A consumer command once accepted --dry-run into **kwargs, dropped it on the floor, and published a package to a public registry. Nothing in the framework could have caught that: honoring a dry run was a convention, and a convention is something a handler can forget. This release makes it structural.
Every command now declares effect="read_only" or effect="mutating" at registration -- no default, no inference from names or tags, a hard error if you omit it. The four reserved flags stop being something each app spells for itself; the framework owns --dry-run, --yes, --quiet and --verbose, so they mean the same thing in every strictcli program, and declaring a flag by one of those names is now a registration error. Side effects ride ctx.effects: run, spawn, write, mkdir, remove, rename, chmod, http. In a dry run the handle records instead of performing and the framework prints the would-do log; in a real run the same handler code executes for real. Handlers never branch on a dry_run kwarg again, because the mode lives in the handle rather than in an argument somebody has to remember to read.
The hard part of previewing is what a recorded mutation returns. Rather than guess, the handle returns an Unsettled carrier. Forward it into a later effect and the preview continues with the provenance rendered inline -- step-output and stale-value brands appear in the log line itself. Try to read it or branch on it and you get a hard error and a truncated preview that says exactly where the preview stopped and why. A short honest preview beats a long invented one, and no amount of inference would have made the guess trustworthy.
Two shipped checks keep the seam visible: effects-bypass follows the call graph reachable from every registered handler and reports direct ambient effects, and observe-allowlist-breadth names any single-token observe prefix broad enough to exempt a whole binary.
This is a breaking release by design: every consumer's commands are unclassified today and will fail registration at their lock bump. That is deliberate -- a silent grace period would have left exactly the bug class this regime exists to kill. The fleet migrates in a dedicated wave right after this release, and the same regime lands in the Go and TypeScript implementations in this same coordinated release, so the three stay in lockstep.
#Breaking
- [strictcli] Breaking: the effects regime. Every command must now declare
effect="read_only"oreffect="mutating"at registration.--dry-run,--yes,--quietand--verboseare reserved framework flag names, delivered on the Context (ctx.dry_run,ctx.yes,ctx.quiet,ctx.verbose) and gatingctx.info/ctx.debug.ctx.effectsmints the eight recorded operations (run,spawn,write,mkdir,remove,rename,chmod,http); under--dry-runthey are recorded, not executed, and rendered as a would-do log, withUnsettledcarriers that forward into later effects and truncate honestly when extracted from. Mutating commands prompt for confirmation unless--yesis passed. A**kwargshandler must declareforwarding=Forwarding(reason=...). A built-ineffects-bypasscheck fails on direct process, filesystem or network calls inside effects-using handlers.
#Features
- [strictcli] **New built-in
observe-allowlist-breadthcheck (severitywarn).** A single-tokenproc_observe_allowlistprefix such as["git"]exempts an entire binary: every matching invocation executes for real under--dry-run, is never logged, and is legal inside aread_onlycommand. The check names each one;--ignore-warningsclears it.
#Fixes
- [strictcli] **
config set,config initandconfig editnow honour--dry-run.** The framework's own mutating commands routed their writes (andconfig edit's$EDITORlaunch) aroundctx.effects, so a dry run printed "no changes were made" while rewriting your config file and opening your editor. Every mutation now rides the handle and is recorded, not performed. - [strictcli] Three effects-regime correctness fixes. (1) The dry-run preview no longer skips numbers: framework-blessed cache writes (the schema dump and test-coverage shards) consumed would-do sequence numbers they never rendered, so a coverage-instrumented dry run began its preview at
2.and every«step N output»brand and truncation "ends at step N" shifted with it — cache writes now carry their own counter. (2)ctx.effects.run'scheckandstreamandctx.effects.http'schecknow reject a forwarded carrier instead of silently ignoring it in dry mode. (3) Writing an attribute on anUnsettledcarrier truncates the preview:__setattr__was unpoisoned, so a forged_brandcould mint a preview line describing nothing and a forged_forwardablecould make a void carrier forwardable. - [strictcli] The confirm prompt accepts a CRLF-terminated answer. A
ytyped at a console whose terminal ends lines with CRLF was read asy\rand declined. The answer's line terminator is now stripped as exactly one newline then exactly one carriage return;ystill declines. - [strictcli] **The
effects-bypasscheck now analyses everything reachable from a registered command handler.** It previously only looked inside functions whose own body mentionedctx.effects, so two shapes escaped completely: a handler that never mentions the handle, and a bypass one helper-call away. Roots are now registered handlers (a.command/.passthroughdecorator, or a name passed ashandler=) plus, as before, any function that uses the handle; the closure follows direct calls to module-level functions transitively. Expect this check to surface findings it previously missed.
#0.34.0
Add is_hermetic() to the check-side ConnectionEnvReader
#Features
- [strictcli] Hermetic detection in checks.
ConnectionEnvReadernow exposesis_hermetic(), letting a check distinguish--hermeticsuppression from an unset connection env (both surface asconnection_env_valuepresent=False) and honor hermetic even when the env is absent.
#0.33.0
Connection env vars: a hermetic-suppressed, app-level env primitive for connection URLs
Context
Adds a third infra-env kind alongside infra roots and handshake vars. A connection env (e.g. a database DSN) is declared once at app level, read lazily with no default, and suppressed under --hermetic so connection-dependent behavior (including checks) skips visibly. Flags bind to it by reference and check functions can read it through the check context.
#Features
- [strictcli] Connection env vars. Declare a hermetic-suppressed connection URL (e.g. a database DSN) at app level with
App(connection_env=...); bind flags to it withconnection_url/connection_env, read it from handlers viactx.connection_env_value(), and from checks via the wrapped check context. Under--hermeticit resolves absent so connection-dependent behavior skips visibly.
#0.32.2
Strict parsing: reject underscore separators in ints and overflow-to-infinity floats
#Fixes
- [strictcli] Fix. Integer values with underscore separators (
1_000) are now rejected, matching the strict-parsing rules of the other implementations. - [strictcli] Fix. Float values that overflow to infinity (
1e999) are now rejected, matching the no-Inf strict-parsing rule of the other implementations.
#0.32.1
cli-test-coverage skips instead of failing when run outside the app's own dev tree
Context
An installed app that runs its checks from a foreign project's directory anchored cli-test-coverage to that foreign cwd, which has no coverage manifest or shard files, so the check failed listing the app's entire command surface as uncovered. The check now applies subject-matter gating: when the anchored coverage root contains neither a manifest nor any shard files, it reports a visible skip naming the anchored path. When either exists, behavior is unchanged.
#Fixes
- [strictcli] The
cli-test-coveragecheck no longer fails when an installed app runs its checks outside its own development tree (e.g. from a consumer project's directory); it now reports a visible skip when no coverage manifest or shard files exist at the anchored root.
#0.32.0
Deterministic cli-test-coverage verdict from the committed manifest; chdir-safe coverage recording
Context
The cli-test-coverage check previously derived its verdict solely from local per-process shard files, so any machine that had not run the suite failed with 'no coverage data' regardless of repo state. The check now derives its verdict from the committed .strictcli/test-coverage.json manifest (union with any local shards), making it deterministic across machines. Coverage recording and the check are also anchored to the app's construction-time directory, so tests that chdir still record into the repo and a check evaluated from a foreign cwd reads the app's own state.
#Fixes
- [strictcli] Deterministic cli-test-coverage verdict. The
cli-test-coveragecheck now derives its verdict from the committed.strictcli/test-coverage.jsonmanifest (union with any local shards), so it produces the same result on every machine instead of failing with "no coverage data" where the suite has not been run locally. Coverage recording and the check are anchored to the app's construction-time directory, so tests that change directory still record into the repo and the check reads the app's own state when run from elsewhere.
#0.31.0
npm wrapper discontinued and MCP error-message parity
Context
The npm distribution of the Python implementation is discontinued. Starting at 0.31.0, the npm name is being reassigned to a native TypeScript implementation. Python users should install strictcli from PyPI.
#Breaking
- [strictcli] npm wrapper discontinued. strictcli on npm becomes the native TypeScript implementation from 0.31.0; Python users install from PyPI.
#Fixes
- [strictcli] MCP error-message parity. tools/call parameter-validation errors now match across implementations, and unknown tools are reported as tool-result errors instead of protocol errors.
#0.30.1
Fix coverage shard directory creation in test contexts
#Fixes
- [strictcli] Bug fix. Coverage shard directory is now created on-demand in
_record_coverage, fixing crashes whenapp.test()runs in a different working directory than where the App was constructed.
#0.30.0
CLI test-coverage instrumentation, notes channel
#Features
- [strictcli] New feature.
App(test_coverage=True)enables CLI test-coverage instrumentation. Everytest()andcall()invocation records the resolved command path. A built-incli-test-coveragecheck merges per-process shard files and hard-FAILs listing uncovered commands.
#0.29.0
#Breaking
- [strictcli] Handler contract redesign. Command and passthrough handlers now always receive
ctxas their first argument (no annotation required), and must returnint(exit code),None(exit 0), orstrictcli.outcome(exit_code, data).Context.emitis removed and any other return value is a hard error.strictcli.outcome(...)is the new way to return structured data. - [strictcli] **Breaking:
versionis now required onApp.** The metadata auto-detect fallback is gone. Migrate withversion=importlib.metadata.version("<dist>").
#Features
- [strictcli] Check reporters gain a note channel and --verbose now shows notes, durations, and a count summary. Check implementations can record informational notes via reporter.note (allowed on any outcome, including a pass) that never affect status or exit codes. Under --verbose, results now render per-check notes, per-check durations, and a trailing pass/fail/warn/skip count summary; JSON output always includes notes and duration_ms fields.
#Fixes
- [strictcli] Canonical float formatting. Floats now render consistently in help defaults,
config show, TOMLconfig setwrites, and choices/validation error messages: shortest round-trip decimal, integer-valued floats keep a trailing.0,-0.0is preserved, fixed notation for magnitudes in [1e-6, 1e21) and scientific (1e+21,1e-7) outside. Fixes malformed output like1e+16.0in error messages. - [strictcli] Compact JSON data output. Handler
outcome(data=...)now prints one compact JSON line (no spaces after,/:), matching the Go implementation byte-for-byte. - [strictcli] **
config set/config set --defaultpreserve comments and key order in TOML config files.** Edits now touch only the changed key instead of rewriting the whole file. - [strictcli] Python-Go parity fixes. @-prefix file/stdin trimming now trims only Go's cutset (space, tab, CR, LF), preserving other whitespace; tag/check/config-field name validation rejects trailing newlines;
config editprintserror: editor failedand exits 1 when the editor fails or is missing; MCP JSON-RPC errors match Go (Parse error,Method not found) and non-object JSON is reported as a parse error; dict flag values render with deterministically sorted keys in help defaults and error messages.
#0.28.0
Check outcome model (sealed reporters, per-problem severity, purity partition), check providers, InfraEnv location roots, global-flag conflict fix, conformance parity.
Context
Breaking changes (minor bump in 0.x):
- CheckResult deleted. Check handlers now receive a ceiling-typed reporter
(ErrorReporter for error-severity checks, WarnReporter for warn-severity) and return outcomes via reporter.passed / reporter.found / reporter.skipped.
- @app.check replaced by @app.error_check / @app.warn_check decorators that
enforce the severity-form contract at registration time.
- Check implementations change from fn(ctx) -> CheckResult to
fn(ctx, reporter) -> outcome.
- Scope adapters return SkipCheck(reason=...) instead of CheckResult("skip", ...).
#Breaking
- [strictcli] Breaking: sealed reporter outcome model. CheckResult removed; check handlers now receive a ceiling-typed reporter (ErrorReporter / WarnReporter) and return outcomes via reporter.passed / reporter.found / reporter.skipped. Registration via @app.error_check / @app.warn_check replaces @app.check.
#Features
- [strictcli] InfraEnv primitive. New location-root concept with RelativeToRoot flag modifier, handshake env vars, and InfraEnv protocol for tools that manage infrastructure directories.
- [strictcli] Check providers. register_check_provider() hook for scoped, lazy per-cwd check materialization. CheckSpec / error_check_spec / warn_check_spec public constructors for provider-supplied check definitions.
- [strictcli] Purity partition. Check runner now splits checks into pure (no subprocess, no network) and impure sets; --dry-run runs pure checks only. Scope-adapter replacement context validated at registration time.
#Fixes
- [strictcli] Fix: global-flag config-conflict now checked in post-command position. Flags placed after the command token are now conflict-checked against config values, matching the pre-command behavior.
- [strictcli] Fix: --dump-schema serializes RelativeToRoot flag defaults machine-stably. Schema output is now deterministic across platforms.
#0.27.0
Public schema-dict accessor, divergence-aware config conflict mode with per-flag override, and validation-only ConfigField/flag coexistence
Context
Phase 2 additions, all backward compatible:
- dump_schema_dict() exposes the CLI schema as a dict with no filesystem or
CWD access (the --dump-schema writer path adds project_id on top).
- config_conflict_mode="error" now only errors when the config and CLI/env
values actually diverge; identical values agree. A per-flag conflict_mode kwarg overrides the app default for a single flag.
- A config field whose name equals a flag's param name is a validation-only
annotation of that flag: it renders once (in config show/init) and its default must agree with the flag's default (registration error otherwise).
#Features
- [strictcli] **
dump_schema_dict().** New publicAppmethod returning the CLI schema as a dict (with version, withoutproject_id), computed with zero filesystem or working-directory access. - [strictcli] **Divergence-aware config conflict mode + per-flag
conflict_mode.** Inconfig_conflict_mode="error", a value set in both the config file and the CLI/env is now only an error when the two values differ; identical values agree and are accepted. A new per-flagconflict_modekwarg overrides the app-level mode for a single flag. - [strictcli] Config fields that share a flag's name are now validation-only. A
config_fieldwhose name matches a flag renders once (on the flag, with its help as a trailing annotation) inconfig showandconfig initinstead of appearing twice. Declaring such a field with a default that disagrees with the flag's default is now a registration error.
#0.26.0
PEP 561 py.typed marker and typed decorator return annotations for consumer type checking.
Context
Adds py.typed marker file for PEP 561 compliance and fixes decorator return type annotations so consumer type checkers (mypy, pyright) see proper types for flag(), arg(), command(), and check() decorators instead of generic Callable.
#Features
- [strictcli] New feature. PEP 561 py.typed marker and typed decorator return annotations -- consumer type checkers now see proper types for flag(), arg(), command(), and check() decorators.
#0.25.0
Config lifecycle overhaul: parse-time loading, --config flag, --hermetic, conflict mode, hard-error loading, ctx.Source provenance API.
Context
Major rework of the config system. Config files are now loaded at parse time rather than lazily, giving deterministic precedence (CLI > env > config > default). The new --config flag lets callers specify an explicit config path. --hermetic disables all config/env resolution for reproducible runs. Conflict mode makes a value set both in the config file and on the CLI (or env) a hard error instead of silently letting the CLI win. Hard-error config loading fails loudly on malformed config files instead of silently ignoring them. The ctx.Source provenance API lets handlers inspect where each flag value came from (cli, env, config, default). Reserved global flag enforcement prevents user code from shadowing built-in flags.
#Breaking
- [strictcli] Reserved global flag names enforced. Global flags named
help,version,dump-schema,config,hermetic, ormcpare now rejected at registration time.
#Features
- [strictcli] Parse-time config loading. Config files are now loaded at parse time (not construction time), ensuring late-written config files are honored.
- [strictcli] **
--configflag andno_default_config_pathoption.**--config <path>selects a config file explicitly;no_default_config_pathrequires explicit--configinstead of searching the default path. - [strictcli] Hard-error config loading and config conflict mode. Malformed TOML and JSON config files produce hard errors with line/column position information.
config_conflict_mode="error"(anAppargument) makes a value set both in the config file and on the CLI (or env) a hard error instead of silently letting the CLI win; the default stayscli-wins. - [strictcli] **
--hermeticflag.** Ignores env vars and config, using only CLI flags and defaults. Mutually exclusive with--configand config subcommands. - [strictcli] **
ctx.Sourceprovenance API.**ctx.source(flag)returns the origin of each flag value:cli,env,config,default, orimplied.
#0.24.2
- No user-facing changes.
#0.24.1
Recovered full release history and added MIT license
Context
53 pre-releasable versioned changelog files were recovered from saferm archive, restoring complete release history in CHANGELOG.md. Project now includes MIT license.
#Features
- [strictcli] New feature. Project now includes MIT license.
#0.24.0
Optional values skip choices validation; WARN check results satisfy depends_on; arg default-type validation.
Context
Choices validation previously rejected optional flags/args that were not passed (None flowed into the choices check, producing "invalid value 'None'"). Validation is now skipped for absent optional values across flags, args, and globals via one shared helper. Check dependency semantics changed: a check that WARNs satisfies its dependents (WARN means passed-with-notes, not failed); only FAIL cascade-skips. Arg default-type validation gains str and list cases.
#Features
- [strictcli] Warn satisfies check dependencies. A check that returns warn no longer causes its dependents to be cascade-skipped; dependents are skipped only when a dependency fails (or was itself skipped). Warn still makes the check run exit non-zero unless
--ignore-warnings.
#Fixes
- [strictcli] Fixed. Unset mutex flags with choices and optional args with
default=None+ choices no longer fail with "invalid value 'None'" when not passed. Custom validators are no longer invoked with None for not-passed flags. - [strictcli] Fixed. Arg defaults are now fully validated at registration: a non-string default on a str arg is rejected, and list arg defaults are validated as non-empty lists of the item type (previously a valid list default on a
list[int]/list[float]arg was wrongly rejected with a scalar-type error). Int elements in float list defaults are coerced to float.
#0.23.0
Registration-time ban on bare --force flag name and --no-* prefix
Context
Flag names 'force' (exact) and names starting with 'no-' are now rejected at registration time. Bare --force encourages agents to bypass guardrails without thinking; qualified names like --force-overwrite make the intent explicit. The no- prefix is reserved for strictcli's auto-generated negation system (--no-flag for negatable bools).
#Features
- [strictcli] New. Registration-time ban on bare --force flag name and --no-* prefix. Flag names starting with no- are reserved for the negation system.
#0.22.0
Required booleans, Context type, schema project_id guard, tag contract global flags fix
Context
Breaking change: Bool flags no longer auto-default to false. BoolFlag without explicit Default(false) is now required. New Context type for structured handler communication. Schema dump now validates project_id. TagContract now checks global flags.
#Breaking
- [strictcli] Breaking. Bool flags no longer auto-default to false. BoolFlag without explicit Default(false) is now required — user must pass --flag or --no-flag.
#Features
- [strictcli] New. Schema dump validates project_id before writing — hard error if existing schema belongs to a different project.
- [strictcli] New. Context type with output methods (Info, Warn, Debug, Error, Emit) for structured handler communication.
- [strictcli] New. Comprehensive sub-project READMEs replacing severely stale documentation.
#Fixes
- [strictcli] Fix. TagContract now checks global flags, not just command-level flags.
#0.21.0
Command declaration framework: structured handler returns, programmatic invocation, typed args, config fields, compound types, tool export, MCP projection, and scope-based check filtering.
Context
This release transforms strictcli from a CLI framework into a command declaration framework with multiple output projections. Commands declared once can be invoked via CLI (existing), programmatic API (app.call/acall), tool export (as_tools with JSON Schema), and MCP server (--mcp flag).
Key additions: handlers can return structured data (backward compatible -- int returns still work), positional args gain type and choices support, first-class config fields with per-command binding and startup validation, compound types (list[T] and dict[str,T]), command visibility (hidden/interactive), schema enrichment with versioning and constraint serialization, and declarative scope field on check definitions with set_scope_adapter() for context-dependent pre-check filtering.
#Features
- [strictcli] New feature. Programmatic invocation via
app.call()andapp.acall()withInvokeErrorexception for structured error handling. - [strictcli] New feature. Schema versioning with
schema_versionfield, constraint serialization (mutex, CoRequired, Requires, Implies), tag contracts, and arg defaults in--dump-schemaoutput. - [strictcli] New feature. Typed positional args with
type(str/int/float/bool) andchoicessupport, matching the flag type system. - [strictcli] New feature. Command visibility:
hiddenandinteractivedeclarations with help text filtering and schema serialization. - [strictcli] New feature. First-class config fields with
config_field()declarations, per-command binding, startup validation,config inittemplate generation, and unknown-key rejection. - [strictcli] New feature. Compound types
list[T]anddict[str, T]on flags and args with projection-specific CLI parsing. - [strictcli] New feature. Tool export:
json_schema(),as_tools(),Tooldataclass, and router tool for AI agent integration. - [strictcli] New feature. MCP projection via
serve_mcp()and--mcpflag for Model Context Protocol tool serving. - [strictcli] New feature. Optional
scopefield on check definitions for declarative pre-check context filtering, andset_scope_adapter()method on App for registering scope transformation callbacks.
#Fixes
- [strictcli] Fix. Validate unknown kwargs and enforce required globals in passthrough invoke path.
#0.20.1
Fix keyword collision in flag parameter names
Context
Flags named after Python keywords (--global, --class, --import, etc.) now produce valid parameter names with a trailing underscore per PEP 8 convention.
#Fixes
- [strictcli] Fix. Flags named after Python keywords (
--global,--class,--import) now produce valid parameter names with a trailing underscore (e.g.,global_).
#0.20.0
Frozen Command, command tags, tag contracts
Context
Breaking: Command dataclass is now frozen=True with tuple fields (was mutable list). New: command tags (string labels with group inheritance) and tag contracts (registration-time validation that tagged commands have required flags).
#Breaking
- [strictcli] Breaking. Command dataclass is now
frozen=Truewithtuplefields instead oflist.
#Features
- [strictcli] New. Command tags: string labels on commands and groups with group-to-command inheritance.
- [strictcli] New. Tag contracts: registration-time validation that tagged commands have required flags.
#0.19.0
#Breaking
- [strictcli] Breaking. Renamed
TagtoFlagSetandtags=parameter toflag_sets=across all API surfaces.
#0.18.0
Public API for running checks programmatically
Context
Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use.
#Features
- [strictcli] New. Public API for running checks programmatically:
App.run_checks(),format_check_results(),format_check_results_json(),CheckRunResult.
#0.17.0
Repeatable flag default validation and help text display
#Breaking
- [strictcli] Breaking. Repeatable flag defaults are now validated at registration: must be a list,
default=[]is rejected as redundant, and element types must match the flag type.
#Features
- [strictcli] New. Help text now displays non-empty repeatable flag defaults as
[default: a, b, c].
#0.16.1
Error message fixes and help text improvements
#Fixes
- [strictcli] Fix.
config setmutex error messages now include theconfig set:prefix for consistency. - [strictcli] Fix. Config set help text now mentions backslash escaping for values containing commas in repeatable flags.
#0.16.0
unique and env_separator flag fields, config array coercion, config set repeatable support
Context
Breaking: config show now requires --plain or --json. New features: unique field enforces no duplicate values on repeatable flags, env_separator controls how env vars are split into arrays, config arrays are coerced to declared types, config set supports repeatable flags with --clear and --default.
#Breaking
- [strictcli] Config show.
config shownow requires explicit--plainor--jsonflag instead of defaulting to plain text.
#Features
- [strictcli] New flag field.
uniquefield on repeatable flags enforces no duplicate values at CLI parse time and in config arrays. - [strictcli] New flag field.
env_separatoron repeatable flags controls how environment variable values are split into arrays. - [strictcli] Config arrays. Config values for repeatable flags are now coerced from strings to the declared type.
- [strictcli] Config set. Repeatable flags can now be managed with
config set, including--clearto reset and--defaultto restore default values.
#Fixes
- [strictcli] Fix. Float NaN/Inf env var error messages now include the environment variable suffix.
- [strictcli] Fix. Config error messages now use consistent type names and array formatting.
- [strictcli] Fix. Flag collection no longer includes config group pseudo-flags, preventing incorrect behavior in commands that enumerate all flags.
#0.15.1
Config show bool parity and negative values in config set
Context
config show now outputs lowercase true/false matching Go. config set accepts negative numeric values.
#Breaking
- [strictcli] Breaking. Config set now accepts negative numeric values. Unknown single-dash tokens produce 'unexpected argument' instead of 'unknown flag'.
#Fixes
- [strictcli] Fix. Config show now outputs lowercase true/false for bool values, matching Go implementation.
#0.15.0
Config set type coercion and key validation
Context
config set now validates keys against registered flags and coerces string values to the flag's declared type before writing. This is a breaking change: unknown keys are rejected.
#Breaking
- [strictcli] Breaking.
config setnow validates keys against registered flags and rejects unknown keys.
#Features
- [strictcli] New feature.
config setcoerces values to the flag's declared type (int, bool, float) before writing to config.
#0.14.0
checks_embed for inline TOML data
Context
Adds checks_embed parameter as an alternative to checks_path, allowing TOML bytes to be passed directly without requiring a file on disk.
#Features
- [strictcli] New feature.
checks_embedparameter accepts raw TOML bytes, enabling inline checks configuration without a file on disk.
#0.13.0
#Features
- [strictcli] New feature. Schema dump (--dump-schema) now includes a project_id field read from pyproject.toml, providing provenance for schema validation.
#0.12.0
#Breaking
- [strictcli] Breaking. CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via
checks_path=. checks.toml now requires a top-levelappfield matching the app name.
#0.11.0
#Features
- [strictcli] New feature.
checks_pathparameter onAppfor explicit checks.toml location, replacing CWD-based discovery.
#0.10.0
#Features
- [strictcli] New feature.
--dump-schemaomits fields matching defaults and includes a top-leveldefaultsobject documenting what missing fields mean. - [strictcli] New feature.
@-prefix for string flag values:@pathreads from file,@-reads from stdin,@@escapes. 1 MB size limit, trailing whitespace stripped.
#0.9.1
#Features
- [strictcli] New feature. Public
config_file_pathproperty on App.
#Fixes
- [strictcli] Fix. Config format validation errors now have parity between Python and Go.
#0.9.0
#Features
- [strictcli] New feature. config_path and config_format options for TOML-based configuration file support.
#0.8.7
#Features
- [strictcli] New feature. Go implementation now includes subcommand name in parse error help suggestions, matching Python.
#Fixes
- [strictcli] Fix. Unknown subcommand errors in groups now suggest group-level help (e.g.,
try 'myapp config --help').
#0.8.6
#Features
- [strictcli] New feature. Parse error messages now suggest the correct subcommand help (e.g.,
try 'app stream --help'instead oftry 'app --help').
#0.8.5
#Features
- [strictcli] New feature. App-level and command-level help output now includes a 'Global flags' section.
#Fixes
- [strictcli] Fix. Auto-registered
checkcommand no longer collides with app-level global flags (e.g.,--dry-run).
#0.8.4
#Fixes
- [strictcli] Fix. Allow empty
tags = []in.strictcli/checks.toml. Checks without tags are valid -- they are addressable by--nameand included in--all.
#0.8.3
- No user-facing changes.
#0.8.2
- No user-facing changes.
#0.8.1
- No user-facing changes.
#0.8.0
#Features
- [strictcli] New feature. Check system -- a first-class, security-hardened check/validation framework. Register checks in
.strictcli/checks.toml(source of truth) with metadata (tags, severity, dependencies), implement them via@app.check()decorator, and run them with the auto-registeredcheckcommand. Includes: tag-based filtering with a set-operation DSL (&,|,^,-,!), DAG-based dependency ordering, human/JSON output,--list/--dry-run/--verbosemodes,--ignore-warnings, and--dump-schemaintegration.
#0.7.1
#Features
- [strictcli] New feature. Handlers with
**kwargssignatures are accepted without strict parameter validation.
#0.7.0
#Features
- [strictcli] New feature.
--dump-schemaflag auto-generates.strictcli/schema.jsondescribing the full CLI structure. - [strictcli] New feature. Opt-in JSON config file support with
App(config=True). Reads~/.config/{name}/config.jsonwith precedence: CLI > env > config > default. Auto-registersconfig show/set/path/editsubcommands. - [strictcli] New feature. Recursive group nesting to arbitrary depth. Groups can contain subgroups via
group.group(name, help=...). - [strictcli] New feature.
type=floatflag support. Rejects NaN and Inf. - [strictcli] New feature.
--helpand-hrecognized anywhere in the argument list, not just at token boundaries. - [strictcli] New feature.
App(version=None)auto-detects version fromimportlib.metadata.
#0.6.1
#Fixes
- [strictcli] Fix. Harmonize Implies and deprecated command error messages with Go for exact parity.
#0.6.0
#Features
- [strictcli] New feature. Deprecated commands: declare a command name + message that prints the deprecation notice and exits when invoked, with a
Deprecated:section in help output.
#0.5.0
#Features
- [strictcli] New feature.
Impliesflag dependency type: when a trigger flag is set, automatically set a target bool flag to a specified value. Explicit contradictions are parse errors.
#0.4.1
#Fixes
- [strictcli] CI publish fix. Fixed publish workflow for GitHub Actions compatibility.
#0.4.0
#Breaking
- [strictcli] Breaking: mutex groups always required. Removed the
requiredparameter fromMutexGroup. All mutex groups now require exactly one flag to be provided.
#Features
- [strictcli] Flag dependencies. New
CoRequiredandRequirestypes for declaring flags that must appear together.
#Fixes
- [strictcli] Choices on global flags. Global flags now validate choices correctly.
- [strictcli] Repeatable global flags. Global flags with
repeatable=Truenow work correctly. - [strictcli] Strict int parsing. Integer flags now enforce 64-bit signed range to match Go behavior.
- [strictcli] Strict int env parsing. Integer environment variables reject leading/trailing whitespace.