Skip to content

Harden the spec fetch, token sweep and config paths - #153

Merged
leet-c1 merged 13 commits into
mainfrom
fix/next-release-hardening
Oct 9, 2026
Merged

leet-c1 merged 13 commits into
mainfrom
fix/next-release-hardening

Conversation

@leet-c1

@leet-c1 leet-c1 commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Hardening items deferred from the v0.9.1 pre-release review, bundled into one PR.

Changes

  • The OpenAPI spec fetch goes through the shared transport. docs openapi, docs endpoints and docs endpoint now get:

    • a 32 MiB response cap;
    • the redirect guard, which refuses a redirect to a different path or host;
    • --debug tracing and --max-retries;
    • exit codes that can be classified: 4 for a 404, 5 for a 429, 6 for a 5xx or a redirect loop, 8 for anything else. These apply when no cache is available.

    When a cached spec exists, the fetch makes one attempt, because the stale cache is the fallback. An explicit --max-retries or C1I_MAX_RETRIES overrides that. Offline with a stale cache, a run takes 139 ms instead of 5.4 s.

    This retires a documented exception in CLAUDE.md. docs search and docs page still bypass the transport.

  • A fresh cache that can't be read triggers a re-fetch instead of an error.

  • A token-sweep race is fixed. The sweep checks the file again just before deleting it, and removes it only if it's still the same file. A fresh token that a concurrent c1i process renames into place is no longer deleted. On Windows, the file id is pinned first, because Lstat loads it lazily.

  • A relative home or config dir is refused. With a relative HOME or %AppData%, c1i no longer writes credentials, tokens or ~/.c1i.yaml under the working directory, and it no longer reads ~/.c1i.yaml from there. Before, a .c1i.yaml planted in a repo could redirect url. If C1I_CLIENT_ID and C1I_CLIENT_SECRET were set, those credentials would be sent to that host.

  • upgrade no longer runs go env. It reads GOBIN and GOPATH from the environment and the Go env file, in Go's own precedence order. Running go env made the Go toolchain on PATH write telemetry under the user's config dir.

  • Uninstall docs now say where the binary lives for each install method, and that Linux follows XDG_CONFIG_HOME.

  • golang.org/x/term is bumped to v0.47.0, which brings x/sys v0.49.0.

The CHANGELOG [Unreleased] section has an Upgrading from 0.9.1 note for scripts. It covers the new exit codes, the default retries when no cache exists, and the relative-home refusal.

Verification

  • Mutation-checked tests: every new test failed on a mutant that removes its fix. Independent reviewers re-ran 30 mutants across two rounds. All relevant ones are killed; the survivors are equivalent behavior, or the Windows-only pin, since CI runs on Linux.
  • Gates with Go 1.27.2:
    • build, and vet for linux, windows and darwin;
    • go test -count=1 -shuffle=on with 3 seeds;
    • golangci-lint (0 issues) and gosec with the CI flags;
    • govulncheck for linux, darwin and windows (0 called);
    • go mod tidy and gitleaks;
    • 7 cross-builds.
  • Live checks:
    • docs endpoints returns 820 rows, and docs endpoint gives a deterministic hash;
    • --debug traces the fetch;
    • an offline stale-cache run returns quickly with the warning;
    • a planted ./.c1i.yaml is ignored when HOME is relative;
    • upgrade --check writes no go/telemetry and still detects a GOBIN install.
  • Reviews: independent code and docs reviews approved after one fix round.

Not included

  • A Windows cmdkey alternative for removing keyring entries is left out of the uninstall docs. It couldn't be verified without a Windows host.
  • A max_retries key in ~/.c1i.yaml doesn't override the one-attempt rule. That matches the README, which documents only the flag and the env var as sources.

🤖 Generated with Claude Code

leet-c1 and others added 13 commits October 9, 2026 17:52
docs openapi, docs endpoints and docs endpoint used http.DefaultClient,
so --debug and --max-retries were inert, a redirect to any host was
followed, and the body was read without a size cap. The fetch now uses
transport.New with both flags and a 32 MiB cap; cache, stale fallback
and parse-before-cache are unchanged. Failures classify like upgrade's
dist fetch: 4/5/6 for a 404/429/5xx, 8 otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cache file inside cacheMaxAge that failed to read (a permissions
error, or a directory at the cache path) was returned as the command's
error without trying the network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pruneTokenDir read an expired <key>.json and then removed the path. A
concurrent store renaming a fresh token over it in between lost the
fresh token. Remove only when a second Lstat is os.SameFile with the
first; on Windows the first stat's file id is pinned eagerly, since
Lstat loads it lazily.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A relative HOME (Linux/macOS) or %AppData% makes os.UserConfigDir
relative, so the credential file fallback and the token file cache
wrote under the working directory. keychain.ConfigDir now refuses a
non-absolute result: Store/Load fail with an error naming the path,
and the token cache treats it as unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Name the removal step per install method (Homebrew, go install, a
downloaded binary, the container image), and note that the Linux config
directory follows XDG_CONFIG_HOME.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pulls golang.org/x/sys to v0.49.0. The outdated-direct-deps query
listed x/term; govulncheck reports nothing called.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
upgrade ran `go env` to detect a go install, and any go command writes
telemetry counters under the user's config dir; GOTELEMETRY=off doesn't
stop it. readGoEnv now follows the go command's order itself: the
environment, then $GOENV or <UserConfigDir>/go/env (none with
GOENV=off), then GOPATH's <home>/go default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With --max-retries defaulting to 4, an offline run with a stale cache
took ~5s, and a blackholed network minutes, before the fallback. When a
cache file exists the fetch now makes one attempt unless --max-retries
or C1I_MAX_RETRIES is set; with no cache the configured retries apply.
The docs also now say a redirect loop exits 6, not 8.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A CDN 403 must exit 8 (not 3) and a 200 that isn't a spec must exit 8
(not 1); both survived mutants replacing distError with a bare error.
APIError also drops the trailing ": " when there is no body.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With HOME="." viper's config path and SaveToConfigFile both resolved to
the working directory, so a planted .c1i.yaml could redirect `url` (and
env-var credentials with it). config.HomeDir refuses a relative home:
the config file is then neither read nor written, and login's save
reports the error as its existing warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add "Upgrading from 0.9.1" for the docs exit codes, default retries and
relative-HOME behavior; stop claiming go-install detection is unchanged
now that Go's env file is read without Go installed; tell agents that a
relative HOME with no keyring makes the auth commands fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eds an absolute config dir

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@leet-c1
leet-c1 merged commit 4a18a36 into main Oct 9, 2026
2 checks passed
@leet-c1
leet-c1 deleted the fix/next-release-hardening branch October 9, 2026 22:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant