gumfield manual
Referencegum v2.0.0 release notesGoogle APIs for agents and terminals

gum v2.0.0

A whole-repository review closed the gap between what the specification promised and what the binary did. The major version changes because several fixes now reject input that earlier versions accepted.

Highlights

  • gum no longer hangs at startup on a Linux host whose Secret Service
  • collection is locked. Every OS keychain call is bounded.

  • csv and markdown are real encoders. Both were in the advertised format
  • enum and both returned TOON or JSON under the requested label.

  • Every result carries the _expression envelope, so a caller can see that a
  • profile dropped fields or collapsed rows. No production path emitted one.

  • MCP tools enforce the inputSchema they advertise. A closed enum, a required
  • property and additionalProperties:false were advisory.

  • tee_mode="failures" writes an artifact, the gain ledger records traffic, and
  • a cache hit gets the same recovery artifact as a cold call. All three were dead paths.

  • Ten security fixes, including a symlinked confirmation signing key, a
  • risk-tier bypass that let gum.read execute a destructive variant, and a shared auth-subject fingerprint across every workload on GCE.

Install

bash
brew install ehmo/tap/gum
# If already installed:
brew update
brew upgrade ehmo/tap/gum
gum --version

The standalone installer remains available:

bash
curl -fsSL https://raw.githubusercontent.com/ehmo/gum/main/install.sh | GUM_VERSION=v2.0.0 bash

Upgrade notes

Most installations need no change. Four cases need action.

1. An expression profile using default_format. Rename the key to format. The specification and docs/expression-profile-dsl.md have always named it format; the parser read default_format, so every documented example failed to validate. The enum widens to toon|csv|json|markdown. raw is rejected, because it names a caller choice that skips shaping rather than an encoder a profile selects; pass --format raw instead. The config key output.default_format is unchanged.

diff
-default_format = "json"
+format = "json"

Validate before upgrading in place:

bash
gum profile validate <path>

2. A profile or call using field_mask_mode="dual_fetch". The mode is rejected with INVALID_ARGS. It promised a second unmasked upstream fetch to feed the recovery artifact and the kernel only ever issued one request. Use field_mask_mode="none" to send no mask, or drop the key.

3. A script calling MCP tools with arguments the schema forbade. Arguments are now validated. In particular gmail_search takes q, not query; flights_search takes departure_date, not departureDate; the four Gmail convenience tools require userId; and gum.code's destructive_scope items are {op_id, resource_key} objects, not strings.

4. A script reading the exit code of gum cache migrate, gum auth use-api-key, or gum auth use-ads-developer-token. All three returned 0 on failures that their own output reported. They now exit non-zero. A script with migrate || handle will start taking the handle branch on RSYNC_AMBIGUITY.

Breaking

  • The expression-profile key is format, not default_format, with the enum
  • toon|csv|json|markdown.

  • field_mask_mode="dual_fetch" is rejected before any upstream request.
  • gum cache migrate exits non-zero on RSYNC_AMBIGUITY.
  • gum auth use-api-key and gum auth use-ads-developer-token exit non-zero
  • when the keychain write fails. A platform with no keychain backend keeps the environment-variable fallback and still exits 0.

  • The nine MCP meta-tools, the two skill helpers and the 18 convenience tools
  • validate arguments against their advertised inputSchema.

  • The 18 convenience tool schemas use the §4.1 argument names of their backing
  • ops. drive.get_file drops mimeType, drive.share drops emailMessage, and gmail_get_message drops format, because no op declares them.

  • gum.read, gum.write and gum.destructive ignore allow_write and
  • allow_destructive in their arguments.

  • gum.code's destructive_scope items are {op_id, resource_key} objects.
  • gum.poll registers RawJsonResult and reports LRO_TIMEOUT as an error.
  • gum.describe_op, gum.gain and gum.cache_stats register the result
  • schemas §2256 to §2258 name. skills_get drops its output schema.

  • TOON keeps the keys of a map whose values are all empty.
  • truncate_strings puts the ellipsis inside the limit, not past it.
  • gum_print encodes a non-string value as JSON.
  • Response numbers decode through json.Number, so an integer above 2^53 keeps
  • its digits.

  • The per-op semantic TTL table is keyed on catalog op ids.
  • gum profile test takes --name, not --profile, to pick one definition
  • from a multi-profile file.

Added

  • gum plugin info <name>. --format=json emits the same object the
  • gum://plugin/{name} MCP resource carries.

  • --max-items on gum read|write|destructive|call and max_items on the
  • three MCP risk tools, replacing the profile's collapse_arrays cap for one invocation. all skips the stage. The override does not change the cache key or the args hash.

  • csv and markdown encoders at stage 8.
  • The _expression envelope on every §13 result shape, and the same
  • information on stderr as a one-line CLI shaping notice.

  • unsupported_capabilities in the catalog ABI, and "partial" in the
  • DescribeOpResult execution_support enums.

  • The documented profile file envelope: [output_profiles."<name>"],
  • [override_bindings] and top-level [[tests]]. Override bindings now reach dispatch.

  • occurrence_count on a deduped row and <field>_truncated siblings from
  • truncate_strings.

  • GUM_KEYRING_TIMEOUT to override the keychain call bound.
  • --destructive-budget and repeatable --destructive-scope on gum code,
  • without which --allow-destructive could never execute.

  • Five profile DSL keys documented that parsed only in Go: projection,
  • flatten_singletons, sort_by, limit, omit_zero_counts.

Changed

  • Stage 1 injects the variant's default_fields as the upstream field mask when
  • a profile states none. No shipped variant declares default_fields, so nothing new goes upstream today.

  • results is a recognised record-array key alongside items, data and
  • messages.

  • The shaping notice states the row counts dedupe and limit removed.
  • _expression.omitted_count counts every row shaping drops, and
  • _expression.lossy reads the cap in force.

  • CSV carries top-level scalars as repeated columns and secondary arrays as one
  • count column each. Markdown no longer truncates cells.

  • Every kernel error carries a §7 envelope. Anything unstructured becomes
  • SERVICE_DOWN with the cause kept in the chain.

  • A non-gum_oauth auth failure carries auth_strategy,
  • missing_components and setup_command, and keeps its own error code.

  • gum plugin remove and reinstall run as registry transactions.
  • Catalog variants sort by variant_id before JCS hashing.
  • gen-catalog validates every variant's output_profile name at build.
  • staticcheck pins to v0.8.1, which can read Go 1.26 export data.

Fixed

  • gum hung forever at startup on a locked Secret Service collection, which
  • covers headless, SSH, container and CI hosts. go-keyring parks on a D-Bus unlock prompt nobody can answer, and every invocation reached it through GrantedScopes. Keychain calls are bounded at 20s interactive, 2s background.

  • tee_mode="failures" wrote nothing, because the write sat past the point the
  • step-7 error path returns from.

  • The gain ledger was never written. Nothing set dispatch.Config.Ledger
  • outside tests, so gum gain reported an empty ledger however much traffic a profile had served. The file was also created 0644 instead of 0600, ignored XDG_DATA_HOME and the profile segment, and did not honour gain.enabled=false.

  • A cache hit skipped the recovery artifact, so a warm response named dropped
  • paths with no artifact to recover them from.

  • TOON Decode cut a string holding a newline in half and returned a nil
  • error. {"a": "x\ny=z"} decoded to {"a": "\"x", "y": "z\""}: truncated, with a dangling quote and a fabricated field. Verified with 30s of FuzzToonDecode, 11.2M executions, no crash.

  • dedupe, sort_by and limit required a top-level array, which no real
  • Google list response is. dedupe also collapsed a result set to one row when its key fields were all absent.

  • k=-1 on gum.search_apis panicked the stdio server.
  • A confirmation token's replay marker was keyed on the caller's hex signature
  • string, so re-spelling that field in upper case bypassed it and one approval authorized unbounded executions.

  • canonicalizeArgs drops null-valued keys at every depth, so an argument
  • spelled as null and an omitted argument share one cache key. Two profiles can no longer collide on the semantic cache key and be served each other's body.

  • gum cache clear --expired counted matched keys, not committed deletions.
  • The HTTP cache migration autocommitted per row, rewrote every gum-cache key
  • into an unreachable form, and wrote every row with ttl 0.

  • Stored plugin credentials reach the subprocess. gum plugin setup wrote them
  • to the keychain and nothing read them back, so every plugin needing a credential failed its canary.

  • gum plugin setup failed on the second credential of a two-credential
  • manifest.

  • gum auth use-oauth-client erased the stored client secret when re-run
  • without a secret flag.

  • gum auth logout skipped the gum_oauth vault purge when no BYO client was
  • registered.

  • --raw and --no-field-mask were parsed and discarded.
  • A typed scalar nested in body:='{"mode":5}' skipped the enum check the flat
  • form gets.

  • The audit log's §11 omit-when-false rule re-added the keys it had just
  • dropped, in the wrong position. No shipped caller passes those keys.

Security

  • A symlinked confirmation signing key is refused on both read paths. The
  • lost-race adopt path followed symlinks, so anyone able to write the data directory could have gum sign destructive-confirmation tokens with a key they control.

  • gum.read with variant_id pinned to a destructive variant executed it,
  • because the risk-tier pre-check compared against the op's default variant while the kernel honoured the pin. The same handler read allow_destructive out of the caller's arguments. Reachable only through a plugin-contributed catalog; no shipped op has variants of differing risk class.

  • gum plugin setup echoed every typed secret to the terminal.
  • Deleting one file disabled the plugin executable trust check. plugins.lock
  • is now authoritative and the digest sidecar must agree with it.

  • A quarantined plugin with no retry time was spawned anyway.
  • A plugin manifest may not claim a host-owned environment variable in
  • needs_user_creds or env_allow, which a manifest could use to have setup prompt the operator for GUM_OAUTH_CLIENT_SECRET by display name.

  • A plugin-catalog schema_hashes value became an unvalidated path segment, so
  • a row carrying json/../../secret read any file the process could reach.

  • On GCE, Cloud Run and GKE every workload derived the same auth-subject
  • fingerprint. That fingerprint keys tee artifact HMACs, gum://results handles, cache entries and gain-ledger rows, so two service accounts sharing a profile directory could read and overwrite each other's artifacts. Resolve now reads the service-account email from the metadata server and fails with ADC_SUBJECT_UNKNOWN when it cannot.

  • The byo_oauth principal fingerprint keys on the account, not the refresh
  • token, which Google rotates on every re-login.

  • Confirmation tokens no longer bind an empty principal.
  • govulncheck reports 0 vulnerabilities that gum's code calls and 0 in
  • packages gum imports. One advisory remains in the module graph: GO-2026-5932 marks golang.org/x/crypto/openpgp unmaintained. gum imports no symbol from that package, and the advisory has no fixed version.

Known limitations

  • No catalog variant declares default_fields. Spec §5.6 curates the table by
  • hand and it was still empty at this tag, so the stage-1 field mask had nothing to inject and --fields completion was empty.

  • Nothing converts a plugin registry variant row into a dispatchable catalog
  • variant, so gum plugin install runs no catalog validation. The plugin contract doc now says so.

  • No catalog variant declares a non-full execution_support, so
  • unsupported_capabilities and "partial" are declared and tested but not exercised by shipped data.

  • field_mask_mode="dual_fetch" is refused rather than implemented.
  • The §10.2 HTTP and ETag cache backend has no production reader or writer.
  • http.db and http-wal.db are created and never used.

  • macOS binaries carry the Go linker's ad-hoc signature, not a Developer ID
  • signature, and are not notarized. Checked on this release's darwin/arm64 artifact: codesign -v exits 0, which meets the Apple Silicon requirement that every executable carry a signature. curl sets no com.apple.quarantine attribute, so neither install.sh nor a Homebrew download reaches Gatekeeper, and a quarantined copy still runs from a shell (checked under Darwin 25.6.0). spctl --assess --type execute reports rejected, and no install path gum publishes consults that verdict. An earlier version of this note told readers to clear quarantine by hand. That advice was wrong.

Token savings

Measured with the release fixtures using a local build stamped 2.0.0. Run from the apps/gum directory of the matching source checkout:

bash
gum gain --fixture-replay --format=toon
gum gain --fixture-replay --format=json
Default format Total calls Total tokens in Total tokens saved Aggregate savings
toon 10 3,922 0 0 %
json 10 3,922 -12 0.31 % overhead

Verification

All seven jobs in the v2.0.0 release workflow passed: tag validation, live docs match, tests, govulncheck, the GoReleaser build, the independent four-platform rebuild, and the provenance comparison.

The four downloaded archives matched checksums.txt, and each one matched its subject digest in gum-v2.0.0.intoto.jsonl. The extracted darwin/arm64 binary matched release-binaries.sha256.

A local rebuild reproduced all four published binaries. The command in Reproducibility below, run from a clean clone at tag v2.0.0 on one darwin/arm64 host with GOTOOLCHAIN=go1.26.7, produced these hashes:

Target sha256
darwin/amd64 e7cfd445523b3ea6bbddd4a24cf728f4c4b75d003f0f1f1a365187962d7304a5
darwin/arm64 332081264900db51e6602826ffc4fa94b2bbe156885185752a27bc0e138c51d0
linux/amd64 4b258174657c55210caef26a0fbe37ff8201d1d3373caf368638859202423af5
linux/arm64 1cc2dba8270dda1b4274732487552070f84f8fc0d49a5f5769b3a182df4fc156

Each hash matches the matching line in release-binaries.sha256.

The Homebrew installation reports 2.0.0 and gum doctor passed every check. brew audit --strict --online --os=all --arch=all ehmo/tap/gum and brew test ehmo/tap/gum both passed.

Reproducibility

bash
git clone https://github.com/ehmo/gum.git
cd gum && git checkout v2.0.0
cd apps/gum
GOTOOLCHAIN=go1.26.7 CGO_ENABLED=0 GOOS=<os> GOARCH=<arch> go build -trimpath \
  -ldflags='-s -w -X main.version=2.0.0' ./cmd/gum
sha256sum gum

All four published binaries were built with go1.26.7. The hashes above were produced by cross-compiling every target from one host, so a single machine can check the whole set.

Build from a full clone, not from a linked git worktree. Go embeds the commit revision in the binary, and it silently skips that stamp in a linked worktree, which changes the hash.