Skip to content

mcli 20260901 Released

Go 1.27.0 with the SILO dependency stack under its own module path, a new read-only checksum verification command, a fix for the JSON metrics panic, fail-closed credential redaction in every trace and error path, strict policy writes, private support artifacts, third-party notices in every artifact, and a release chain that proves what it ships: signed tags, exact-main CI evidence, Sigstore attestations, and immutable releases.

Published: 2026-09-01 · Version: RELEASE.2026-09-01T00-00-00Z

Twenty-six days after mcli 20260806, this release carries everything the client accumulated since. The toolchain moves to Go 1.27.0 and the shared dependency stack is aligned with the SILO server, with the SILO package imported under its own module path so go.mod says what the binary is built from. The first genuinely new command since the fork arrives — mcli checksum verify, a read-only audit of stored S3 additional checksums against the bytes an endpoint actually returns. The panic that made --json admin prometheus metrics unusable is fixed. And every path that can print a credential — --debug traces, admin trace, error messages, JSON error documents, checksum reports — was rewritten fail-closed after three rounds of adversarial review. The release process changed as much as the code: artifacts are produced only from a GPG-signed tag at the exact tip of main with green CI for that commit, ship LICENSE, NOTICE, and a generated CREDITS in every format, carry Sigstore build provenance, and land in immutable GitHub releases.

The release was built from ddac5d58, the merge of pgsty/mc#24: 49 commits, 262 files, +12,487 / −6,089 lines since RELEASE.2026-08-06T00-00-00Z.

Warning

Behavior changes

  • macOS 13 is the new floor. Go 1.27 links darwin binaries with a minimum OS version of 13.0; the darwin archives no longer run on macOS 12 and earlier.
  • mcli --json admin prometheus metrics works again and returns metric families in deterministic name order. Plain-text metrics output is unchanged.
  • pipe with empty standard input now performs a regular zero-byte PUT instead of a one-part multipart upload. The resulting ETag is d41d8cd98f00b204e9800998ecf8427e rather than the <md5>-1 form. Non-empty input is unchanged.
  • Policy writes are validated strictly on the clientadmin policy create, admin user svcacct add/set, idp ldap accesskey create/edit. A bare ARN such as arn:aws:s3::: is rejected (write arn:aws:s3:::* to mean every resource under that prefix); a named policy without Version is rejected; an empty policy is rejected; mixing S3 and admin actions in one statement is rejected. The last three were already refused by the server, so only bare ARNs change what can be stored. Reads of existing policies remain permissive. Session policies for service accounts may still omit Version, and admin user svcacct set --policy still accepts an empty document — that is the way to clear an inline policy and return the account to its inherited one.
  • A malformed global flag before the command name is now an error. mcli -H "bad" ls, --resolve and --limit-* values that fail to parse at the app level used to be silently ignored and the command ran without them. They now fail with the same message the command-level form produces, reported once on stderr — or as a JSON error document under --json — where the CLI library used to echo the text to stdout first.
  • Error messages no longer echo secrets. alias set reports only the length requirement for a bad secret key; MC_CONFIG_ENV_FILE parse errors name a line number instead of repeating the line; SSE-C key errors withhold the key; admin config set masks secret-bearing key=value pairs; remote replication targets are printed without credentials; a malformed --custom-header entry is reported by position, never by value.
  • Support artifacts are private. Files written by support diag, support profile, support perf, support inspect, admin cluster bucket export, and admin cluster iam export are created with mode 0600, including the rotated backup of a previous file. They were 0644 or 0666 before.
  • --debug shows less. An Authorization header keeps only its scheme (AWS4-HMAC-SHA256 **REDACTED**); tokens, cookies, API keys, SSE-C keys and every --custom-header value are redacted; 2xx response bodies are no longer dumped. admin trace output is redacted the same way in both its default and its verbose rendering: header values by name, query strings, bodies, error text and annotations.
  • The User-Agent reports minio-go/v7.3.0 (the SDK’s version constant) instead of minio-go/v7.0.98.

Major Changes

  • Go 1.27.0 across the module, the container build stage, Linux/macOS/Windows CI, cross-builds, tidy checks, and vulnerability analysis. The Go-maintained module family is current: x/net 0.58.0, x/sys 0.47.0, x/term 0.45.0, x/text 0.41.0, x/crypto 0.55.0, x/mod 0.40.0, x/sync 0.22.0, x/tools 0.49.0. golangci-lint leaves the product module graph and is installed independently at the pinned v2.13.1.
  • The SILO package under its own module path. go.mod now requires github.com/pgsty/silo-pkg/v3 v3.13.0 directly. The client’s policy validation depends on helpers that exist only there — policy.ParseConfigStrict and policy.Resource.IsBareARN — and a replace of upstream minio/pkg would not have been inherited by any project consuming this module as a dependency (pgsty/mc#12), so the requirement is declared honestly instead. Upstream minio/pkg survives only as an indirect dependency of colorjson, held at v3.6.1 by the dependency-floor check, and gomodguard blocks the upstream import path from returning. minio-go moves from v7.0.99 to the upstream pseudo-version v7.3.1-0.20260828014306-0e78d3f18efe that silo-pkg itself builds against; the interim pgsty/silo-go fork is retired, its one functional change having been merged upstream. The rest of the shared stack follows the server: the etcd client modules move from 3.6.9 to 3.7.1, which carries the fix for GO-2026-6107 / CVE-2026-73500; grpc-gateway 2.29.0, zap 1.28.0, and go.yaml.in/yaml/v3 3.0.5 follow. go-systemd deliberately stays at 22.6.0 through SILO’s shared portability pin — 22.7.0 does not compile on NetBSD.
  • Runtime version floors are guarded. A first dependency pass let the removal of development tools silently lower a dozen runtime selections — among them shoenig/go-m1cpu, whose older releases crash during initialization on newer Apple Silicon when CGO is enabled — and was rejected. buildscripts/check-dependency-floors now compares every requirement in go.mod against the previous RELEASE.* tag and fails the build on a regression; the only permitted exception is the go-systemd pin.
  • mcli checksum verify (design). Verifies stored S3 additional checksums — CRC32, CRC32C, CRC64NVME, SHA1, SHA256, of type FULL_OBJECT — against the logical object bytes returned by the object API, for one object, a prefix with --recursive, all versions with --versions or an exact --version-id, or a JSON Lines candidate manifest. It reads with If-Match on unversioned objects and re-stats afterwards, so a write that races the verification is reported as UNKNOWN_OBJECT_CHANGED rather than as a mismatch. Each object yields one of MATCH, MISMATCH, NO_CHECKSUM, WOULD_VERIFY (dry run), one of ten UNKNOWN_* classifications, or one of three SKIPPED_* results; the summary carries objects, a verified count (MATCH + MISMATCH, the only results that actually recomputed a checksum), per-status counts and incomplete. --fail-on accepts mismatch, unknown, no-checksum (fails when any object lacks a checksum or when nothing was verified at all), any (default: mismatches, unknowns, and objects skipped by --max-size), or none. --report writes the same records to a new 0600 JSON Lines file. Results go to stdout whether or not stdout is a terminal, and with --json they are compact JSON Lines outside a terminal — the non-TTY silence reported as pgsty/mc#5 is fixed. Dry runs, --max-workers, --max-size, time filters, and SSE-C through --enc-c are supported. The command is verified against a real SILO server in CI.
  • Credential redaction, fail-closed. Three rounds of adversarial review by Codex showed that the original redaction — and its first two rewrites — recognized the shapes it expected and let everything else through: access keys containing a slash, an Authorization value whose first field is empty so the second one hides, non-hex signatures, secrets tucked into the credential scope or SignedHeaders, Proxy-Authorization, cookies, X-Api-Key, response trailers, a redirect Location with userinfo, a server that reflects request headers into an error body, a JSON-escaped secret inside --json error output, a short token that appears twice. The final design has three rules and no exceptions. An Authorization-class value keeps only its scheme. A header whose name looks like a secret, or that the caller supplied through --custom-header, is redacted without inspecting its content. Text the client did not generate itself — response bodies, server error messages, trace notes — is scanned for credential shapes and then scrubbed of every secret literal the process has learned. That last layer is backed by a process-wide secret registry: alias secret keys and session tokens, MC_HOST_* values, SSE-C keys in every encoding, tier and service-principal secrets, GCS credential files, LDAP passwords and the STS credentials they return, SUBNET tokens, --secret-key/--account-key/--api-key flags, and secret-bearing key=value arguments are registered as they are read, before validation and before any network call. Occurrences are collected as intervals and merged before replacement; JSON error documents are scrubbed on the decoded value tree, string leaves only, so keys and compact non-terminal formatting survive. Forty-odd adversarial cases, a black-box test that runs the real client against an endpoint that reflects everything it receives, and live captures against a SILO server pin the result.
  • admin trace redaction. Server-supplied header maps for other clients’ requests take a path the S3 tracer never touched. Both the human-readable and the JSON rendering, in the default and the verbose form, are now produced from redacted copies — the event is shared between them and never mutated. Query strings (a presigned URL’s signature, a session token in the first parameter), bodies, error text and annotations are scrubbed with the event’s own header values, so a token another client sent cannot reach privileged trace output. Headers are redacted by name: a --custom-header another client sent under a name that does not look like a secret is shown as the server saw it.

Fixes

  • JSON metrics no longer panic. mcli --json admin prometheus metrics failed for both the v2 and v3 metrics APIs with panic: Invalid name validation scheme requested: unset in the 20260806 client. madmin-go built the zero value of the Prometheus text parser, which prometheus/common 0.66 and newer reject. The client now constructs the parser with UTF-8 validation, returns ordinary parse errors instead of panicking, and emits families in name order.
  • pipe with empty input performs a regular PUT; the one-part multipart upload it used to issue for zero bytes was needless and produced a multipart-style ETag.
  • RPM payload validation aligned with the signing script, so an RPM whose contents do not match the release metadata is refused before it is signed.
  • sql no longer panics after printing its rows. The SDK’s select reader closes the HTTP response itself on every terminal event, and the client closed it a second time; on a zstd-compressed response the two drains raced and about two in five queries against a SILO server ended in panic: invalid memory address with exit status 2 after the rows had already been written (pgsty/mc#23). The client now cancels the request and drains the result pipe instead of closing the response again; 820 consecutive queries on a live server ran without a panic where the previous client failed 21 of 50.

Hardening

  • Operator-produced files are no longer world-readable. support diag warned that its archive may contain sensitive environment information and then wrote it 0666; the shared moveFile helper relaxed every 0600 temporary file to 0644 on its way to the final name, and a 0644 left by an earlier client was carried into the rotated backup even when source permissions were preserved. Every support artifact is now created 0600 and chmodded before the first byte is written.
  • Test_FullSuite fails on panic. The functional suite recovered from panics to run cleanup and then reported success; a panic anywhere in the suite, including in the binary build, was invisible to CI.
  • A leftover play/ alias in the new command’s help — the only file in the repository still advertising MinIO’s operated demo alias — is replaced, and the brand gate now rejects bare aliases in command examples, not only hostnames.
  • AGPL notices completed on six fork-authored files that carried only the first half of the license text.

Engineering and Delivery

  • Third-party notices in every artifact. CREDITS is generated by buildscripts/gen-credits.sh from the union of the runtime modules linked into all six GoReleaser targets and is compared in CI so a dependency change cannot leave it stale. LICENSE, NOTICE, and CREDITS ship in the GoReleaser archives, the DEB/RPM/APK packages, and every container image under /licenses/.
  • CI runs on main. Go and Crosscompile ran only on pull requests, so the merge commit — the one that gets tagged — was never tested; both now run on every push to main. Their concurrency group was keyed on github.head_ref, which is empty outside pull requests, so every manual run cancelled every other; it is now keyed on the pull request number with the ref as fallback. VulnCheck also runs weekly, because a fork of an archived upstream can go weeks without a commit. The functional suite in CI now targets SILO RELEASE.2026-08-06T00-00-00Z.
  • Release only from signed tags. The artifact workflow has no manual trigger; artifacts come from a pushed RELEASE.* tag, and the event SHA, the commit the tag points to, and the checkout must all agree. verify-release-tag.sh requires an annotated tag with a valid OpenPGP signature from the expected key — fingerprint and key location come from repository variables rather than from the tree the tag points at, and the imported key must match the expected fingerprint. check-release-commit.sh requires the tagged commit to be the exact tip of origin/main and demands a successful push-to-main run of four workflows for that SHA, matched by workflow file path; pull request and manual runs are not evidence. check-release-state.sh (pgsty/mc#6) runs serially per tag, replaces exactly one existing Draft from scratch, refuses a published release, and re-checks for a single Draft before packages are uploaded. Every one of these decisions lives in a shell script with fixture coverage; the fake gh in the tests pins both the verdicts and the arguments passed, verbatim.
  • Provenance and immutability. Archives, checksums, DEB and APK packages carry Sigstore build provenance (gh attestation verify <file> --repo pgsty/mc). RPMs are excluded on purpose: sign-release-rpms.sh rewrites their bytes with a detached GPG signature from key 9592A7BC7A682E7333376E09E7935D8DB9BD8B20 while the release is still a Draft, and an attestation made earlier would name a digest no published RPM has. GitHub releases are immutable once published; fixing an asset means a new tag. The pgsty/mc container image is built only from attested archives, after gh attestation verify binds each one to release.yml@refs/tags/<tag>, the release commit digest, GitHub-hosted runners, its own filename and its SHA-256; the unpacked binary’s ELF machine type must match the archive’s architecture; each per-architecture image is pulled, run, and checked for the tag before the manifest is assembled, and latest is written only from the two-architecture manifest. The UBI base image is pinned by digest and kept current by Dependabot.
  • Repository governance. main requires a pull request and all eight checks by exact name (DCO, Go on Linux/macOS/Windows, vetchecks, Crosscompile, VulnCheck, Test Release — the last now unfiltered by path, on every pull request and push); RELEASE.* tags can be created only by administrators and can never be updated or deleted; secret scanning, push protection, Dependabot alerts and security updates are on. The project has a single maintainer, so the enforceable policy is “pull request gated on required checks” rather than independent human review; the README says so.
  • Verified before tagging. tidy/verify, dependency floors, go vet, golangci-lint, actionlint, brand policy, govulncheck with zero reachable vulnerabilities; native Apple Silicon CGO execution and the full race suite; the cross-build matrix plus FreeBSD, OpenBSD, NetBSD, and Windows ARM64; the HTTP and HTTPS functional suites against the published SILO RELEASE.2026-08-06T00-00-00Z, including SSE-C, multipart, watch, mirror, presigned requests, users, and policies. On top of that, three review rounds by Codex (GPT-5.6) with independent verification by Claude Code, and a final pass by six independent reviewers over the whole diff since 20260806 — credential redaction, checksum verify, behavior changes, the release chain, the module-path sweep, and a black-box run of the candidate against the 20260806 binary on a live server. 34 of the 49 commits respond directly to something those reviews found, each pinned by a regression test before the fix. Beyond the credential work, the reviews caught — and this release fixes — a --fail-on no-checksum that could exit 0 when nothing had been verified, a release gate that would have accepted runs which never built the release commit, an attestation subject check that would have failed the first real container publish, a moment where latest pointed at an amd64-only manifest, a default admin trace rendering that printed query strings unredacted, credential shapes that rewrote ordinary prose in error messages, a svcacct set that could no longer clear a policy, and the long-standing sql panic.

Compatibility

The full --help tree of this release was diffed against the 20260806 binary: the only difference is the new checksum command. Side by side on the same server, head, cat, get, cp, mirror, find, share, sql, stat, and ls produce identical output and JSON field sets; twenty real-world policy shapes are accepted or rejected identically except for bare ARNs; admin policy info --json is byte-identical. The mc command name and the mcli package name; the ~/.mc / ~/.mcli configuration directories; the github.com/minio/mc module path and all import paths; MC_* environment variables; protocol headers (x-minio-*) and the minio-go user-agent prefix; the minio tier type; the .part.minio transfer suffix; the minio-job scrape job name; and the package formats, asset naming, and YYYYMMDDHHMMSS.0.0 version scheme are unchanged. The client remains compatible with MinIO servers and other S3-compatible endpoints. Projects that consume this module as a Go dependency now resolve github.com/pgsty/silo-pkg/v3 directly and need no replace of their own for it.

Note

Known issues and boundaries

  • checksum verify reports UNKNOWN_READ_ERROR, not MISMATCH, when the server’s own integrity checks refuse a read — SILO’s bitrot protection rejects a corrupted shard before the object API returns it. A MISMATCH means the stored checksum describes different bytes than the object API returns, which is the historical write-path defect the command exists to find; an UNKNOWN is never reported as a MATCH.
  • Access key IDs are redacted wherever they appear inside a credential structure, but a server that reflects a bare access key into an error message will still have it printed: it doubles as the user identifier in legitimate messages. Secrets shorter than three characters are not registered. When the secret key equals the access key — the default minioadmin:minioadmin — the identifier is redacted from error text too, because the string is the secret.
  • --json error documents list the keys of cause.error alphabetically rather than in struct order; the keys and values are unchanged.
  • mcli sql reports a failed query on stderr but still exits 0 (pgsty/mc#25); per-object errors are reported without failing the run so a recursive query can continue. Unchanged from upstream.
  • govulncheck traces GO-2026-5932 in the indirect dependency x/crypto/openpgp; no fixed version exists and the client does not call it.
  • minio/mc#5139 continues to apply: mirror --remove --watch can delete a live object from the target when a non-current version of it is removed from the source.
  • Quality debt that does not block the release — archive reproducibility, test-tool portability, deferred coverage of KMS / SSE-S3 / multi-site replication — is tracked in pgsty/mc#21.
  • 6686b64: chore(deps): align the SILO Go dependency stack
  • 423490d: fix: stop JSON metrics from panicking
  • 14b05e6: build: generate and package third-party notices
  • e938cea: fix: align RPM payload signing validation
  • 8158605: release: adopt Silo Go SDK v7.3.1
  • d6ba236: ci: allow manual release workflow runs
  • b6b2655: ci: release an existing tag on dispatch
  • 70a2950: fix: preserve embeddable pkg compatibility
  • aa07b1c: feat: add read-only object checksum verification
  • 1b39423: fix: preserve checksum output in pipelines
  • a31a42b: docs: record checksum verify output contract
  • 2121081: fix: validate policy writes strictly
  • 712bf3a: fix: use regular PUT for empty pipe input
  • 9a3805d: ci: make release retries tag-idempotent
  • 7bc9843: test: make report permissions portable
  • 2d5abc2: chore: complete the AGPL notice on fork-authored files
  • f5c5dc9: build: import the SILO package under its own module path
  • a8e7eb4: ci: test main on push and stop dispatch runs cancelling each other
  • b75d28b: fix: stop checksum verify help from advertising the play alias
  • c05da7b: chore: drop configuration for bots that are not installed
  • a29b455: fix: stop –debug from printing credentials
  • 08244df: fix: keep secrets out of error messages and traces
  • 30a7ea0: fix: stop writing support artifacts world-readable
  • 14bd724: test: fail the full suite when it panics
  • 4abbb3a: feat: expose how much checksum verify actually verified
  • f484d0d: ci: make a release prove its commit, and pin what it builds from
  • 2c33a4e: chore: ignore the local consumer-module review scratch directory
  • bdc742a: fix: close the remaining –debug credential leaks
  • 3998392: fix: withhold SSE-C keys and credential URLs the parser rejects
  • c1358c4: fix: force 0600 on rotated support artifacts too
  • 45e5144: fix: fail –fail-on no-checksum when nothing was verified
  • 3674933: ci: make the release gate provable and its claims true
  • 8b7b2e9: test: exercise checksum verify against a real server
  • 4b36b0a: fix: make credential redaction fail-closed and scrub final output
  • a6c14da: ci: verify build provenance before repackaging archives into images
  • 915cfd9: fix: reject a malformed app-level global flag instead of ignoring it
  • 81e2767: fix: rebuild SigV4 traces from known fields and widen secret scrubbing
  • 625713c: fix: keep only the scheme of a signed header and scrub JSON structurally
  • ec12b64: fix: redact admin trace output and register every credential ingress
  • 1445e88: ci: release only from a signed tag at the tip of main, verify exact provenance
  • f188939: fix: close final credential and release verification gaps
  • f5b88dd: fix: redact the default admin trace and stop the shape sweep mangling prose
  • 59989f8: fix: let svcacct set clear a policy again, report bad global flags once
  • d38412a: fix: classify an SSE-C refusal with a key as a read error, keep policy and size messages clear
  • b291b53: fix: register a URL password as written, keep listing parameters visible in traces
  • 9dfca8f: test: drive the SSE-C refusal classification through the verify path
  • 76b2e89: fix: keep an unquoted DSN password whole and match auth schemes case-insensitively
  • 328efee: fix: register the password before the first separator of an unquoted DSN payload too
  • 7057705: fix: avoid double-closing S3 Select responses