Configuration reference

Every environment variable, YAML key, and CLI flag that configures Chainsaw, with defaults and precedence.

Every knob that configures a Chainsaw deployment: environment variables, YAML keys, and server flags, with their defaults.

Most of this configures the chainsaw-proxy server. A subset applies to the free CLI — the CHAINSAW_SERVER target, the telemetry toggles, and the offline switches. A server-only value set for the CLI has no effect.

If you are on managed Chain305, almost nothing here needs setting: these are the knobs behind a deployment you run yourself, whether that is the CLI on a workstation, a self-hosted proxy, or the K8s admission webhook.

Every environment variable the server and CLI read is on this page. That is enforced: a build fails when code reads a variable this page does not document.

Contents

Precedence

For the Go server, highest → lowest:

env vars → explicit CLI flags → DB-persisted settings → YAML config → hardcoded defaults

Note the middle two: the settings table outranks the YAML file, not the other way round. That is deliberate and it is what makes the admin UI work — a blocking-mode or ClamAV toggle set in the dashboard writes a settings row, and that row has to survive the next restart even though the YAML on disk still says something else.

The YAML is a seed, not a live source. Passing --config imports the file into the settings table (keys the operator declared explicitly overwrite; the rest seed-if-absent), and from that point the DB row is what the server reads. A boot without --config never re-reads the file at all — the server WARNs when it detects a YAML edited in place since the last import, because those edits do not take effect until you pass --config again.

YAML still wins in one important case: a key the settings table does not hold. The store is layered onto the boot config rather than replacing it (config.OverlayFromStoreForOrg), so an absent row leaves the YAML/flag/default value standing instead of resetting it to the Go zero value.

This section previously read “env vars → CLI flags → YAML config → DB-persisted settings”, which was wrong in two ways. YAML did not outrank the DB — the DB copy replaced it wholesale — and CLI flags were silently dropped on any boot that did not also pass --config, because the store-loaded config replaced the flag-patched one. Both are fixed; flags are now re-applied after the store merge, so an explicitly-passed flag really is the highest non-env source.

Every field of the config struct must either round-trip through the settings layer or be listed as deliberately ephemeral with a reason; a reflection test (core/config/roundtrip.go + roundtrip_test.go) fails CI when a new field is neither.

This precedence holds for every key. It did not always: loadAndPersistConfig used to replace the whole config with a settings-table-hydrated copy built from an explicit key list, so any YAML block the list omitted was reconstructed at its zero value. The store is now layered over the YAML/flag/default config — a settings row wins for the key it holds, and a key with no row keeps the value already in memory. See B29 for which keys the table does and does not hold.


PART A — FEATURE FLAGS

A1. Mode / umbrella switches

FlagDefaultEffect
CHAINSAW_OFFLINEoffDisables Trivy auto-update, Billy LLM, telemetry, device-code OAuth
CHAINSAW_INTEL_BUNDLE_PATH—Path to a chainsaw-intel-bundle-*.tar.gz (W4). When set, refreshable providers (cve, kev, malware, ghsa-swift) read snapshots from the bundle instead of phoning home. See docs/OPERATIONS.md#install-airgap.
CHAINSAW_INTEL_BUNDLE_IDENTITYChain305/chainsaw release.yml on a v* tagOIDC subject regex for the intel-bundle Sigstore signature. Override when publishing your own bundles.
CHAINSAW_INTEL_BUNDLE_SKIP_VERIFYoffDev-only escape hatch — disables the Sigstore signature check on the intel bundle. Logged loudly at startup.
CHAINSAW_INTEL_BUNDLE_STRICT_VERIFYoffOpt-in FULL Sigstore authenticity for the intel bundle (Fulcio cert chain + Rekor inclusion + OIDC issuer + signer identity) on top of the always-on digest binding. With it off, a loaded bundle is integrity-checked but its authenticity is NOT verified. Off by default only because no release.yml run has yet published a real .sigstore bundle — turning it on today hard-fails every current bundle. Same effect as chainsaw bundle verify --strict. Truthy: 1/true/yes/on. Read in core/intelligence/bundle.go.
CHAINSAW_OFFLINE_FAIL_MODEcondition-defaultAdvisory only — not enforced. Reported by chainsaw doctor --offline; no enforcement path reads it. Setting closed does not block installs. For enforcement use CHAINSAW_COVERAGE_MODE below.
CHAINSAW_COVERAGE_MODEoffOptional fail-closed gate. off (default, no behaviour change), warn (report what closed would refuse, refuse nothing), closed (refuse when a required source could not be evaluated). On the K8s admission webhook this is a config surface over the existing fail-mode, not a second gate: closed → fail-mode closed; off and unset are no-ops (they never loosen admission’s default); warn is accepted and logged but changes nothing, because admission’s DB-unavailability refusals are pre-existing enforcement rather than a preview of this gate. The two knobs combine by strictness — neither may loosen the other, and every override is logged at startup. required/grace/max_ledger_age are validated there but unused (admission has no coverage ledger). See plan_optional_fail_closed.md.
CHAINSAW_COVERAGE_REQUIRED(empty)Comma-separated data sources that must be evaluable: malware, cve, typosquat, provenance, registry_metadata, checksum, install_scripts, hidden_unicode. An unknown name is a hard config error, not a silent no-op. Required when mode is warn/closed. On the server (proxy + publish) only the first five are accepted — those surfaces evaluate package metadata and never read artifact bytes, so requiring an artifact-bound source there would be a control that can never fire. The server refuses to start rather than run one; the workstation guard, which does read bytes, accepts all eight. Which of the 25 marketed signals each name backs — and the seventeen signals that have no source name in v1 — is tabulated in COVERAGE_SOURCES.md.
CHAINSAW_COVERAGE_GRACE30sHow long a source that was healthy very recently still counts as evaluable, so a brief upstream hiccup does not refuse a build. Must be shorter than CHAINSAW_COVERAGE_MAX_LEDGER_AGE. Effectively inert on the proxy and publish surfaces: grace needs a strictly earlier healthy observation, and a single intelligence report carries one collection timestamp, so the server-side producer has no cross-scan history to draw on. It is a working rail on the workstation guard. Use warn mode, not grace, to size upstream-blip tolerance on a server.
CHAINSAW_COVERAGE_MAX_LEDGER_AGE15mOuter bound on how old a coverage observation may be. Always wins over grace, so a cached-while-healthy scan cannot vouch for a source during an outage.
CHAINSAW_COVERAGE_BREAK_GLASSoffEscape hatch: disables the coverage gate for one invocation. Logged loudly on stderr every time.
CHAINSAW_ALLOW_INSECURE_TLSoffProcess-wide gate that must be enabled before any per-remote / per-integration TLSInsecureSkipVerify flag (webhooks, registries, SIEM destinations) is honoured. Fail-closed: with it off, those admin-set skip-verify flags are ignored and TLS is always validated. Resolved by (*config.Config).AllowInsecureTLS() in core/config/offline.go, mirroring CHAINSAW_OFFLINE — env var wins, then YAML runtime.allow_insecure_tls, then false. Earlier revisions of this table documented the default as on; that was never true of the code. Enabling it logs a loud “unsafe in production” warning at startup.
CHAINSAW_SELF_HOSTEDoffDisables cloud-only features
CHAINSAW_HAoffHigh-availability mode marker
CHAINSAW_EXPERIMENTALoffShow experimental CLI commands — not implemented. No Go, shell, or UI code reads this name; setting it has no effect. Row kept so the name is not re-documented as live.
CHAINSAW_VERBOSEoffCLI verbose output. Env fallback for the --verbose/-v global flag (flag wins). See B27d. CLI flags.
CHAINSAW_QUIEToffSuppress CLI progress/chatter (results and block reasons are still emitted; never changes an exit code). Env fallback for the --quiet/-q global flag (flag wins). See B27d. CLI flags.
CHAINSAW_ENV—Environment label (dev/prod). Setting it to production (case-insensitive) makes the server refuse CHAINSAW_TEST_MODE=1 at boot.
CHAINSAW_WAVE—Rollout wave bucket — not implemented. No code reads this name. Do not confuse it with the live CHAINSAW_WAVE4_* feature flags (§A5), which are a different mechanism: those are built at runtime as CHAINSAW_WAVE4_<SIGNAL>_<ECOSYSTEM> by provider_wave4_rtt.go. Row kept so the name is not re-documented as live.
CHAINSAW_TEST_MODEoffQA-smoke test mode. Must be exactly "1" to enable. Activates two surfaces, each invisible (404) in production: (a) the test clock (POST /api/admin/test-clock/advance, GET /api/admin/test-clock/now) and (b) the DB killswitch (POST /api/admin/test-dbkill/arm, GET /api/admin/test-dbkill/status). All four endpoints are global-admin only. Refused at boot when CHAINSAW_ENV=production. See docs/TESTING.md#testing-test-mode.
CHAINSAW_TEST_CLOCK_OFFSET0sInitial offset applied to the test clock at boot (Go duration). Ignored unless CHAINSAW_TEST_MODE=1; a stderr line is logged when set without test mode.

A2. Risk / intelligence engine

FlagDefaultEffect
CHAINSAW_INTELLIGENCE_REFRESH_ENABLEDonPeriodic risk-data refresh loop
CHAINSAW_INTELLIGENCE_RECOMPUTE_ENABLEDonSecond phase of each refresh tick: sweeps intelligence_reports rows stamped with a superseded matcherEpoch and re-scans them. Without it a verdict fix never reaches coordinates anyone has already looked at — the primary walk pages package_metadata, which eleven write paths (lockfile scanner, intel scan, transitive fan-out, cache warm, publish prerun, MCP friction, admin scan, upload trigger, artifact followup, and the walk’s own new-version discovery) never populate. Set 0/false/off/no to disable. Gated by CHAINSAW_INTELLIGENCE_REFRESH_ENABLED — turning the refresher off disables both phases.
CHAINSAW_INTELLIGENCE_RECOMPUTE_MAX_ROWS500Rows the recompute sweep may claim per tick. Deliberately well under the uncapped primary walk beside it, so a post-epoch-bump sweep drains steadily rather than stampeding upstream registries. Raise it to drain a large backlog faster after several epoch bumps; watch the chainsaw_intel_recompute_backlog gauge and the chainsaw.intel_recompute alert group, which fires when the backlog is non-zero and nothing has been swept for an hour.
CHAINSAW_INTELLIGENCE_STALE_REFRESH_ENABLEDoffOpt-in sweep that re-scans stored reports older than the refresh window which the primary walk never visits (they have no package_metadata row). Default OFF, unlike the two sweeps above: it reaches registries that rate-limit us, across a population several times the primary walk’s.
CHAINSAW_INTELLIGENCE_STALE_REFRESH_MAX_ROWSadaptiveStale reports the sweep above may refresh per tick. Set, it is an exact cap. Unset (or unparseable / <= 0), the budget adapts: the sampled backlog spread over one CHAINSAW_INTELLIGENCE_REFRESH_MAX_STALENESS window of ticks, never below 200 and never above 800. A fixed 200 let a cohort of reports aging out together grow the backlog 4,605 → 7,392 in a day (2026-09-25/26).
CHAINSAW_INTELLIGENCE_MIN_SERVEABLE_EPOCHCurrentMatcherEpochStaged-deploy override; leave unset in steady state. The epoch floor a cached row must meet to be served. Raising CurrentMatcherEpoch retires every existing row at once, and the transitive-risk provider drops a superseded dependency instead of rescanning it — so a large backlog means every rollup is direct-only until the sweep drains (500/tick by default). Hold this at the OLD epoch during the deploy, raise CHAINSAW_INTELLIGENCE_RECOMPUTE_MAX_ROWS to drain, then unset and redeploy. Serving the old floor during the drain is not a regression: it serves exactly what production already serves. Values that are unparseable, <= 0, or above CurrentMatcherEpoch fall back to CurrentMatcherEpoch rather than widening the cache. The drain-side predicates ignore this deliberately, so the backlog count stays honest. See docs/RUNBOOKS.md#runbooks-matcher-epoch-backfill.
CHAINSAW_INTELLIGENCE_COVERAGE_RECOMPUTE_ENABLEDonThird phase of each refresh tick: re-rolls intelligence_reports rows whose transitiveCoverage.complete is false — a rollup computed against a dependency closure that was only partly in cache when the parent was scored. The scanner runs the tree overlay BEFORE it enqueues dependency scans, so a cold first scan persists a direct-only rollup and nothing went back once the children arrived. Measured on the production export: 699 rows scored against a partial closure, 296 since warmed, and 122 serving a verdict strictly better than their tree warrants. Unlike the matcher-epoch sweep this issues no upstream requests — the root’s own facts are unchanged, so the overlay is re-run against the current cache. Set 0/false/off/no to disable.
CHAINSAW_INTELLIGENCE_COVERAGE_RECOMPUTE_MAX_ROWS2000Rows the coverage sweep may examine per tick. Higher than the matcher-epoch budget because the ceiling here is local CPU and cache reads rather than a third party’s rate limit. A row is only WRITTEN when its closure actually grew, so examining a row whose dependencies never arrived is cheap and idempotent. Watch chainsaw_intel_coverage_backlog together with chainsaw_intel_coverage_improved_total: this backlog does not decay to zero (a package whose deps are not in the estate stays incomplete forever), so the alertable condition is the improved rate sitting at zero while the backlog GROWS.
CHAINSAW_INTELLIGENCE_ADVISORY_GATE_ENABLEDonLets a newly published advisory reach a stored report on the next refresh tick instead of waiting for the coordinate’s next rescan. The primary walk skips any row whose report is younger than the staleness bound, and a new advisory makes a report wrong without making it old — so the skip gate consults the live OSV index and declines to skip when the corpus asserts a CVE the stored report does not carry. The decision is a map lookup against data the tick has already loaded, so it issues no upstream request and no extra query; only a coordinate a new advisory actually matches costs a scan, and that is the scan it was going to get a sweep cycle later anyway. Bounded to one forced scan per coordinate per bundle generation. Set 0/false/off/no to disable. Gated by CHAINSAW_INTELLIGENCE_REFRESH_ENABLED, and dormant when no OSV bundle is loaded.
CHAINSAW_INTELLIGENCE_ADVISORY_GATE_MAX_ROWS200Coordinates the advisory gate may force a rescan for per tick. Matches the stale-report sweep’s budget because it competes for the same upstream budget: at the measured ~9.22 upstream requests per refreshed coordinate that is ~1,850 requests in the worst tick, against the refresher’s measured ~3,400/hour. The cap exists because one advisory landing on a widely-depended-upon package would otherwise turn a single bundle swap into a scan of every affected row at once. Per-tick, not a total — a larger population drains over successive ticks.
CHAINSAW_NEW_VULN_ALERTS_ENABLEDonAlerts when a refresh turns up a new vulnerability affecting inventory. Default-on for fresh deployments. Operators upgrading a deployment with stale snapshots should set 0 until one refresh cycle completes, or the first cycle alerts on the entire backlog at once. Any value other than 0/false/off/no/disable/disabled is treated as on.
CHAINSAW_VERDICT_RECALL_ENABLEDonVerdict recall: alerts when a refresh flips a cached coordinate to malicious, to a suspected typosquat, to a changed publisher, to a newly-present install script, or to a strictly worse risk verdict. The vuln alerter above covers CVEs and nothing else, so without this the daily delta is silent for every non-CVE signal. Each alert mints a finding under a synthetic policy id recall:<trigger>, writes an events row so SIEM sees it, and names the clients that already pulled the coordinate in the last 90 days. Unlike the vuln alerter, an upgrade backlog does NOT need a manual hold: an alert only pages when someone actually pulled the coordinate, and a per-org cap of 50 pages/hour turns any overflow into one digest naming the suppressed count. Findings always mint at full severity — suppression applies to the page, never to the record. Set 0/false/off/no/disable/disabled to turn it off; any other value, including unset, is on.
CHAINSAW_TRUSTSCORE_ATTESTATION_FIRSTonAttestation-first trust scoring: provenance acts as the substrate the score is built on. Set false (or 0/off/no) to revert to the legacy +25 additive provenance contribution — intended for score continuity during a staged rollout, not as a permanent posture.
CHAINSAW_KEV_DISABLED(unset = KEV on)Set to any non-empty value to skip the CISA KEV catalog entirely. With KEV off, the patch-priority leaderboard falls back to the EPSS ≥ 0.5 heuristic instead of the real catalog.
CHAINSAW_KEV_FEED_URL(CISA catalog URL)Override the KEV feed endpoint — an internal mirror, typically.
CHAINSAW_KEV_FEED_PATH—Load the KEV catalog from a local JSON file instead of fetching it, for air-gapped deployments. An unreadable or unparseable file logs a WARN and falls back to the network fetch rather than leaving the index empty.
CHAINSAW_CACHE_WARM_DISABLEDoffKill switch for the background warm-up scans of a freshly scanned package’s pinned direct dependencies. Only the exact value 1 disables it. A recovery valve for a pod that would otherwise fan out across hundreds of packages at boot. Read in core/intelligence/cache_warm.go.
provenance.offline (YAML)offDisable all provenance verification
provenance.swift_full_verify (YAML)offSE-0391 CMS cryptographic verification
swift.git_fallback_enabledonGit-to-registry translator. Clones the URLs an operator supplied via swift.identifier_map_path and synthesizes SE-0292 responses from git tags. SwiftPM resolve does not work through the proxy without it. Set the row to false to opt out; an explicit false always beats the default.
swift.github_conventionoffAuto-translate (i.e. guess) scope.name → github.com/scope/name.git. Off by default — see the security note below. Turning it on requires a non-empty swift.github_org_allowlist; the proxy refuses to start otherwise.
swift.github_org_allowlist(empty)GitHub orgs the convention may guess into. Mandatory whenever swift.github_convention is on; case-insensitive; blank entries do not count.

Security note — these two Swift knobs are different risks and deliberately do not share a default. Do not align them.

git_fallback_enabled clones a URL the operator wrote down. github_convention guesses a URL from a package name, and nothing in SE-0292 binds an identifier to a repository. With the convention on and no allowlist, SwiftPM asking for acme.utils makes Chainsaw clone github.com/acme/utils — so anyone who registers the org acme on GitHub has their code served as the legitimate package, by the security proxy itself. That is scope squatting, and the proxy would be the delivery mechanism.

Wave AF flipped github_convention on to make Swift resolve work out of the box. That contradicted the documented posture and has been reverted. The supported way to resolve Swift packages is swift.identifier_map_path — an explicit identifier → git-URL map, which works with the convention off and is not guessable. Use the convention only when you also pin swift.github_org_allowlist to orgs you control or trust.

Enforcement is two-layer and fails closed: config.SwiftConfig.ValidateForRuntime rejects the unconstrained combination at boot, and swift.NewIdentifierMap rejects it again at the construction choke point (which also catches a github_convention: true set inside the identifier-map file). Both errors name the setting. Air-gapped deployments are covered too: the Swift git fallback now honours CHAINSAW_OFFLINE / runtime.offline and refuses network git operations. | swift.trust_swift_root | off | Trust Apple’s Swift Package Collection root | | malware.enable_ghsa (YAML) | on | Supplementary GHSA malware detection |

A3. Auth / signup / security

FlagDefaultEffect
CHAINSAW_ENABLE_SIGNUP / NEXT_PUBLIC_*trueShow signup link / allow new accounts
CHAINSAW_ENABLE_PASSWORD_RESET / NEXT_PUBLIC_*onAllow password-reset flow. Set =false to disable.
CHAINSAW_ENABLE_MAGIC_LINKonMagic-link auth (default-on when Postmark is configured; logs the link in plaintext otherwise)
CHAINSAW_ENABLE_EMAIL_VERIFICATIONautoVerify email at signup
CHAINSAW_ENABLE_2FAonTOTP 2FA. Operators that haven’t yet set CHAINSAW_TOTP_ENCRYPTION_KEY see “not fully configured” only at enrollment time. Set =false to disable.
CHAINSAW_STRICT_JWTon (1)Refuse to start without persistent JWT secret
CHAINSAW_REQUIRE_ORG_SLUGonMandatory org slug in requests. Single-tenant deployments using legacy /repository/{repo}/{path} URLs opt out with =false.
CHAINSAW_REQUIRE_SIGNATUREon (1)CLI installer (scripts/install.sh) enforces GPG signature on checksums.txt. Set =0 to bypass.
CHAINSAW_SWAGGER_UIoffServe the interactive /swagger HTML UI. Off by default: production deployments do not need an anonymous-reachable page listing every admin endpoint, and when off the route 404s from the same dispatch table so its absence is not even fingerprintable. Accepts 1/true/yes/on. The machine-readable spec at /api/openapi.yaml and /api/openapi.json stays anonymous either way, so SDK generators keep working.
CHAINSAW_RBAC_SCOPES_ENABLEDoffResource-scoped RBAC (Phase 1, ships dark). Off, no request path consumes a resource scope and authorization is byte-for-byte the org-wide behaviour. Truthy (1/true/on/yes/enable/enabled) makes the auth paths in internal/server/authcore honour scope predicates; only findings is scopable. The enforcement phases that would make it safe to turn on are deferred, so do not set it on a deployment that has not completed that wiring. Read in core/tenancy/scopes.go.
FORCE_HTTPSfalse (compose) / true (HA)HTTPS on cookies
CHAINSAW_ALLOW_INSECURE_TLS (test)falseSkip TLS verify in tests

A4. Policy enforcement

FlagDefaultEffect
--blocking-mode / blocking_mode (YAML)trueHook violations block requests
--allow-anonymous / repository_anonymous_accesstrueAnonymous repo reads
repositories[].enabledtruePer-repo on/off
repositories[].anonymous_accessfalsePer-repo anonymous reads
CHAINSAW_ALLOW_PRIVATE_UPSTREAMSoffAllow upstream URLs that resolve to RFC1918 / loopback / link-local IPs. Dial-time only — it does NOT make a private webhook or SIEM URL savable; the create/update validators refuse those regardless. Never re-opens cloud-metadata literals.
CHAINSAW_ALLOW_CGNAT_UPSTREAMSoffAllow RFC 6598 carrier-grade NAT (100.64.0.0/10) destinations — the range Tailscale uses, so this is the knob for a tailnet-hosted SIEM or webhook. Narrower than the flag above on purpose: a tailnet need is one /10 wide and should not also buy loopback and link-local. Honoured by the dialer AND both validators, because a URL that cannot be saved cannot be delivered to. Does NOT re-open 100.100.100.200 (Alibaba cloud metadata), which sits inside that /10.
--tls-insecurefalseSkip TLS verify (single-shot)
CHAINSAW_ADMISSION_FAIL_MODEclosedK8s admission webhook posture when the policy DB is unreachable AND no fresh cached decision is available. open returns allow (the availability-first escape hatch); closed returns deny. Governs DB-unavailability only — a malformed custom rule still fails open at the per-image level. Precedence: combined with CHAINSAW_COVERAGE_MODE by strictness, so open here does not defeat CHAINSAW_COVERAGE_MODE=closed; see that row. See docs/OPERATIONS.md#migrations.
CHAINSAW_ADMISSION_CACHE_TTL30sFreshness window for cached admission decisions used as the FIRST fallback when the DB is unreachable. Go duration. Cache rides out Postgres rolling-restart / pgbouncer reload windows without flapping into fail-mode. Stale entries fall through to CHAINSAW_ADMISSION_FAIL_MODE.
CHAINSAW_OPA_SKIP_VERIFYoff (verification ENFORCED)Do not use in production. Canonical incident-recovery escape hatch that skips Sigstore verification of the OPA policy bundle on every reload. Truthy values: 1, true, TRUE. Each skipped reload emits a loud slog.Warn. The default is now ENFORCED (the pre-cutover preE5DefaultSkip constant in internal/server/policy_dsl_load.go:43 is false); see docs/ENFORCEMENT_AND_POLICY.md#policy-signed-bundles and internal/policy/dsl/signing/.
CHAINSAW_OPA_BUNDLE_SKIP_VERIFYoffLegacy alias for CHAINSAW_OPA_SKIP_VERIFY, preserved for runbook compatibility. Same semantics, same loud warning. New documentation should reference the canonical name. Read in internal/server/policy_dsl_load.go:23.
CHAINSAW_OPA_BUNDLE_IDENTITY(canonical signer regexp)Override the pinned OIDC subject regexp for operators publishing their own signed bundles. Default pin: DefaultPolicySignerIdentityRegexp.

A5. Scanners / detection

FlagDefaultEffect
CHAINSAW_DOCKER_LAYER_SCANon (on|off|auto)Per-layer Trivy scan in Docker pipeline
CHAINSAW_DOCKER_INSPECTORboth (eol|oci|both)Wave-X, default flipped eol → both in Wave AF. Selects the layer inspector wired into the Docker scanner. eol = curated EOL-base-image allowlist (Wave V, prod-verified), preserved as the air-gapped escape hatch. oci = OCI manifest walker with per-layer apk/dpkg/rpm extraction. both = OCI primary + EOL fallback. Unset and unknown values land on both so a typo does not regress coverage. Read in cmd/chainsaw-proxy/main.go::selectDockerInspector.
CHAINSAW_OCI_PLATFORMlinux/amd64Wave-AA. Platform descriptor the OCI inspector selects from a multi-arch index. Format <os>/<arch> (e.g. linux/arm64). Default linux/amd64 matches chainsaw prod regardless of runtime.GOARCH, so M-series dev boxes still walk the amd64 manifest CI scans. Malformed values silently fall back to the default. Read in internal/hooks/docker_oci_inspector.go::resolvePlatformPreference.
clamav.enabled (YAML)falseClamAV scanning
CHAINSAW_CHECKSUM_MODE—log|quarantine|block
CHAINSAW_SCAN_FAIL_ON_UNSCANNEDunsetThe CI-friendly half of --fail-on-unscanned. When truthy, a scan that could not evaluate a candidate exits non-zero instead of warning. It can only raise strictness, never lower it: an explicit --fail-on-unscanned=false on the command line wins, and on chainsaw scan-repo — where the flag already defaults ON — this variable changes nothing. Read in core/cli/scan_gate.go::resolveFailOnUnscanned.
CHAINSAW_GUARD_SERVER_BLOCK_SEVERITYhighMinimum CVE severity at which the signed-in install preflight refuses (critical, high, medium, low). Rows below the threshold are printed as a non-blocking ! warning … (server: <sev> — allowed) rather than dropped, so nothing is hidden. Set low to restore the pre-0.20 behaviour of refusing any known-vulnerable package — note npm ci fans the whole lockfile through this preflight, so one low CVE anywhere then fails the install. An unrecognised value is a loud error, never a silent “block everything” or “block nothing”.
Datasources *.enabled (openssf, trivy_db, epss, clamav_db)variesToggle individual data feeds
*.startup_syncvariesSync feed on startup (auto-disabled in offline mode)

Deprecated compatibility knob: --trivial-path / hooks.trivial.binary_path are accepted but ignored because the scanner runs in-process. Configure the in-process scanner with hooks.trivial.db_path, hooks.trivial.timeout_seconds, and hooks.trivial.max_concurrent_scans.

A6. Observability / telemetry

FlagDefaultEffect
CHAINSAW_METRICS_ENABLEDonPrometheus /metrics endpoint. Set =false/0/no/off to disable.
CHAINSAW_METRICS_TOKEN(unset)Bearer token that admits a Prometheus scrape of /metrics (Authorization: Bearer <token>). Unset or blank = no scrape is admitted and /metrics answers 401 like any other authenticated route; the proxy is internet-facing and the exposition is operational detail. Compared in constant time.
CHAINSAW_TELEMETRY_DISABLEDfalseDisable telemetry collection (forces off everywhere)
CHAINSAW_TELEMETRY_ENABLED—Inverse opt-in form
CHAINSAW_TELEMETRY_DEBUGfalseVerbose telemetry logging
CHAINSAW_OFFLINEfalseUmbrella kill switch — disables every phone-home path (telemetry included). Also refuses chainsaw guard update, the CLI’s one networked command; override for a single run with chainsaw guard update --allow-network. Truthy values are matched case-insensitively after trimming (1, true, yes, on), consistently across the guard, coverage and intelligence paths.
DO_NOT_TRACKunsetHonoured as a full telemetry opt-out, per the cross-tool DO_NOT_TRACK convention. Any truthy value maps to ModeDisabled — nothing is sent and no install_id is minted. The bare DNT spelling is deliberately not consulted (it is an HTTP-header convention and the short name collides).
CHAINSAW_NO_NUDGEfalseSilence the free install-guard conversion CTAs (does NOT affect telemetry consent)

CLI telemetry is consent-gated, default deny. The chainsaw CLI emits nothing until a human grants consent (chainsaw telemetry on, or the first-run prompt). Declined, undecided, and unreadable consent state all mean do not send — so a CI runner that never answered a prompt transmits nothing, and chainsaw telemetry off genuinely stops the session events, not just the guard events. The consent gate runs before the client is constructed, so a non-consenting run opens no socket and writes no install_id. See TELEMETRY.md. | CHAINSAW_REFUSAL_SHARING | false | Proxy/server only: opt in to sharing refused-package identifying data off-box (fail-closed). The free local guard does NOT use this — it shares identifying data only after first-run consent | | OTEL_EXPORTER_OTLP_INSECURE | true | Disable TLS for OTLP endpoint | | CHAINSAW_EVENTS_PARTITIONED | false | Monthly partitioning of events table | | CHAINSAW_UI_DEBUG_HTTP / NEXT_PUBLIC_* | false | HTTP request/response logging in UI |

Free install-guard telemetry is consent-gated, not env-defaulted. The guard asks once on the first interactive run and sends nothing until the user opts in; CI/non-TTY collects nothing until chainsaw telemetry on. chainsaw telemetry on|off persists the decision; the kill switches above still force-off regardless. See TELEMETRY.md. | CHAINSAW_USE_FALLBACK_DATA / NEXT_PUBLIC_* | false | Mock data fallback in UI |

A7. Operational toggles

FlagDefaultEffect
CHAINSAW_NO_UNICODEunsetRenders CLI output with ASCII-only glyphs. Set to anything other than an explicit falsey value to disable Unicode; CHAINSAW_NO_UNICODE=0 declines to force it OFF but does not force it on. It deliberately outranks the terminal-capability probe, so it is the escape hatch for an exotic or non-UTF-8 locale the probe reads wrongly. Read in core/cli/output.go and core/cli/unicode_decide.go.
CHAINSAW_UNICODEunsetForces Unicode glyphs ON in CLI output for a capable console the terminal probe does not recognise. Truthy: 1/true/yes/on. Loses to CHAINSAW_NO_UNICODE. Read in core/cli/unicode_decide.go.
CHAINSAW_REAPER_DRY_RUNfalseLog would-be deletions without acting
CHAINSAW_META_STORE_DUAL_WRITEfalseCutover dual-write to sidecar+postgres
CHAINSAW_XREPLICA_SINGLEFLIGHToffCross-replica deduplication
CHAINSAW_REVERSE_ETLoffReverse-ETL worker
--reset-admin-passwordfalseRegenerate admin password on boot
SKIP_CLEANUP (test)falseKeep test artifacts

A8. UI feature flags (ui_new/)

FlagDefaultEffect
NEXT_PUBLIC_ONBOARDING_V2trueOnboarding redesign (set 0 to disable)
PostHog dynamic flags (via useFeatureFlag)per-userServer-driven flag evaluation
?sample=1 query / localStorage chainsaw.sample-modeoffDemo/sandbox mode
sessionStorage chainsaw.featureIntro.pendingunsetOnboarding tour step tracking

A11. Background workers (env-gated, default-OFF)

Each gate is env-mode and opt-in. Producer-side wiring (where applicable) is unconditional so the underlying tables grow even when the consumer is off; this keeps the next operator-flip cheap.

FlagDefaultEffectRead in
CHAINSAW_FEEDBACK_TUNER_ENABLEDoffGates the feedbacktune consumer loop. Producer (SQLEmitter into feedback_events) is always on.cmd/chainsaw-proxy/init_feedback_tuner.go
CHAINSAW_OWNERSHIP_SLA_ENABLEDoffGates the ownership-routing SLA escalation loop. Independent of the ownership_routing_lifecycle PostHog flag (which gates the producer-side insert).cmd/chainsaw-proxy/init_ownership_sla.go
CHAINSAW_BYPASS_HISTORY_ENABLEDoffGates the bypass-confidence-history snapshotter. Consumer-side gate (canResolveBypassExemption) is always wired and falls back to legacy single-shot check on empty table.cmd/chainsaw-proxy/init_bypass_history.go
CHAINSAW_DATACLEANUP_ENABLEDoffGates the retention worker that prunes feedback_events, hardening_approval_nonces, guard_block_events and guard_installs.cmd/chainsaw-proxy/init_datacleanup.go
CHAINSAW_DORMANCY_WORKER_ENABLEDdeployment-derivedThe one worker in this table whose default is not simply off: unset means ON for the hosted control plane and OFF for self-hosted deployments, so on-prem operators opt into the churn signal the same way they opt into every other phone-home path. An explicit 1/true/yes/on or 0/false/no/off always wins in either direction; an unrecognised value falls back to the deployment default rather than guessing. The emit stays per-org consent-gated and leader-elected downstream, so a default-on hosted worker cannot profile an opted-out org.cmd/chainsaw-proxy/init_dormancy.go
CHAINSAW_EXCEPTION_REMINDER_ENABLEDoffGates the exception-expiry reminder loop. It emits user-visible notifications, so it stays silent until an operator has reviewed the expiries the org has accumulated.cmd/chainsaw-proxy/init_exception_reminders.go
CHAINSAW_STALE_FINDING_REMINDERS_ENABLEDoffGates the stale-finding reminder loop. The first tick after enabling fires T-30d notifications for every legacy open finding, so plan a mute window for the rollout.cmd/chainsaw-proxy/init_stale_finding_reminders.go
CHAINSAW_SBOM_SNAPSHOT_ENABLEDoffGates the scheduled SBOM snapshotter (scheduled-trigger snapshots).cmd/chainsaw-proxy/init_sbom_snapshotter.go
CHAINSAW_SECURITY_DIGEST_ENABLEDoffGates the security-digest worker. Off, the worker never starts and the data-source refresh endpoint’s digest trigger is a no-op.cmd/chainsaw-proxy/init_security_digest.go
CHAINSAW_FLYWHEEL_CAPTURE_ENABLEDoffGates the intelligence-flywheel capture cron (T1), which folds consenting orgs’ malicious/typosquat blocks into flywheel_signals (org COUNT only, never identity). The three flags below only take effect when this one is on.cmd/chainsaw-proxy/init_flywheel.go
CHAINSAW_FLYWHEEL_AUTOPROMOTE_ENABLEDoffLets an eligible, high-confidence signal tier to auto_promoted (T3) instead of human_pending. Must stay off until false positives are driven down. Interlocked: it has no effect unless CHAINSAW_FLYWHEEL_CANARY_COHORT_ENABLED is also set — set alone it is refused with a WARN and auto-promotion stays off.cmd/chainsaw-proxy/init_flywheel.go
CHAINSAW_FLYWHEEL_CANARY_COHORT_ENABLEDoffThe auto-promote interlock: asserts a pre-global canary cohort exists. No cohort-scoped canary is built yet (the T5 monitor is a post-publish circuit breaker, not a cohort gate), so leave it off.cmd/chainsaw-proxy/init_flywheel.go
CHAINSAW_FLYWHEEL_PUBLISH_ENABLEDoffGates the T4 publish-back pass: confirmed curation rows into the malware index overlay.cmd/chainsaw-proxy/init_flywheel.go
CHAINSAW_FLYWHEEL_CANARY_ENABLEDoffGates the T5 block-rate monitor and auto-halt.cmd/chainsaw-proxy/init_flywheel.go
CHAINSAW_MONITORED_TARGETS_SWEEPoffProcess-level switch for the declared-inventory re-scan sweep; the per-org monitored_targets_sweep flag is checked on top of it. Off by default because one customer declares 19,083 coordinates and switching it on at upgrade would be a self-inflicted load event. Truthy: 1/true/yes/on.internal/server/monitored_targets_sweep_wiring.go
BILLY_LEDGER_RETENTION_ENABLEDoffGates the retention worker that prunes billy_call_logs and billy_safety_events (the latter can echo user-typed text).cmd/chainsaw-proxy/init_billy_ledger_retention.go

A13. Optional / experimental scanners + intelligence (env-gated, default-OFF)

Single-instance escape hatches. Each flag is a process-level toggle the operator flips on the chainsaw container; no PostHog, no DB persistence. Truthy = 1 / true / yes / on (case-insensitive). Wave-4 flags also accept a comma-separated ecosystem allowlist (e.g. npm,pypi) for per-ecosystem canarying.

FlagDefaultEffectRead in
CHAINSAW_PATCH_SIMULATOR_ENABLEDoffEnv-var fallback that force-enables the patch-simulator surface when no PostHog flag is configured. Fails closed on any error so the surface stays invisible.internal/server/patch_simulate.go
CHAINSAW_PUBLIC_DEEP_SCANonKill switch for the public deep-scan endpoint — the only path in the product where the PUBLIC surface originates registry egress. Set 0 or false to disable without a rollback. no and off do NOT work here — this switch uses strconv.ParseBool, which rejects them, and an unparseable value falls back to ENABLED. That is the opposite spelling set from CHAINSAW_CAPABILITY_SCAN below, which does accept no/off; the inconsistency is real and is why both rows spell it out. Added days after a scan storm took that same surface down (plan_scan_backpressure.md), so an operator can stop it if deep scan is ever the thing hammering a registry.internal/server/public_intelligence_deep_scan.go
CHAINSAW_MAX_CONCURRENT_SCANS8Process-wide ceiling on on-demand package scans in flight. Foreground scans queue up to 2s then get 429 CHW-1305 with Retry-After; background follow-ups never queue and are dropped. A non-positive or unparseable value falls back to 8 rather than being honoured — “0 concurrent scans” would disable the feature silently, which is the failure this admission gate exists to stop. Read scan_admission.go’s sizing note before raising it: the arithmetic that once justified 8 is withdrawn, and nobody has measured real per-scan connection demand.internal/server/scan_admission.go
CHAINSAW_CAPABILITY_SCANonOpt-OUT since 2026-09-15 (capability-scan-enabled-2026-09-15.md): set 0/false/no/off to disable. Enables the npm-tarball capability scan (extracts to a temp dir and runs internal/capability.Analyze). Purely additive — never modifies existing signals or fails the proxy request.internal/server/artifact_capability.go
CHAINSAW_ATTESTATION_ENABLEDoffWave A1 attestation output. When truthy, a successful publish that was allowed by an evaluated policy decision mints a DSSE-wrapped SLSA VSA (verification_summary/v1) stored beside the artifact at <storage path>.vsa.json. Needs CHAINSAW_ATTESTATION_SIGNER as well — flag on, signer empty mints nothing. Never mints for a blocked publish, for a Maven .sha1/.asc/maven-metadata.xml sidecar, or when no policy bundle was evaluated, and a mint failure never fails the upload.internal/server/upload_attest.go
CHAINSAW_ATTESTATION_SIGNERunsetThe attestation signing key. A filesystem path to a PEM private key (PKCS#8 ed25519 or ECDSA P-256, or SEC1 EC PRIVATE KEY on P-256) signs locally. A scheme:// URI signs through a cloud KMS — enterprise build only (-tags enterprise; the free build refuses with a “provider not built” error naming the missing package): awskms:///arn:aws:kms:<region>:<account>:key/<id> or awskms:///alias/<name> (AWS SDK credential chain: env, shared config, IRSA, instance role), gcpkms://projects/<p>/locations/<l>/keyRings/<r>/cryptoKeys/<k> (Application Default Credentials), azurekms://<vault>.vault.azure.net/<key> (Azure identity: AZURE_* env, workload/managed identity, or Azure CLI), hashivault://<key> (Vault transit; VAULT_ADDR, VAULT_TOKEN, optional TRANSIT_SECRET_ENGINE_PATH, default transit). KMS keys must be ECDSA P-256 or RSA ≥ 2048; the key id is fetched on first use, so a KMS that is down costs those attestations (counted as mint failures) without needing a restart once it recovers. Vault is tested end to end; AWS, GCP and Azure are untested against a real cloud. Empty (the default) means attestation is off regardless of the flag above.internal/server/upload_attest.go → internal/attest/signer.go, internal/attest/kms.go
repositories[].apt.signing_key (YAML)unsetWave A2 index signing for a hosted apt repository. Same reference forms and credentials as CHAINSAW_ATTESTATION_SIGNER, resolved by the same code: a path to a PEM private key, or a KMS URI (enterprise build). The key must be ECDSA P-256 or RSA ≥ 2048 (ed25519 is refused for OpenPGP). A KMS that is unreachable, rejects the credential or lacks the key fails the regen closed. Set: every regen publishes dists/<suite>/InRelease + Release.gpg and the public key at <repo>/chainsaw-index.asc; a signer that fails writes nothing, and an index request with no signature yet gets 503 CHW-8520 plus a scheduled regen — never an unsigned index. Unset: output is byte-identical to before, and a previous run’s signatures are removed. Stored with the repository’s format_options; operator-only (no API writes it), and inherited by every org seeded from the entry. See ONBOARDING.internal/formats/apt/signing.go, internal/server/index_signing.go
repositories[].yum.signing_key (YAML)unsetSame as above for a hosted yum/dnf repository: publishes <releasever>/<basearch>/repodata/repomd.xml.asc per tree (enforced client-side by repo_gpgcheck=1) and one chainsaw-index.asc at the repository root. .rpm files are never re-signed.internal/formats/rpmrepo/signing.go, internal/server/index_signing.go
CHAINSAW_WAVE4_NONEXISTENT_AUTHORoffWave-4 RTT signal: package author has no upstream account / has been deleted. Accepts 1 (all ecosystems) or <eco>,<eco> (allowlist). rubygems only, whatever the value: npm’s user endpoint answers 401 and PyPI’s a 200 bot challenge for real and missing accounts alike, so neither can ever answer the probe.internal/intelligence/provider_wave4_rtt.go
CHAINSAW_WAVE4_FIRST_TIME_COLLABORATORoffWave-4 RTT signal: commit author is publishing under this name for the first time. Accepts 1 or <eco>,<eco>.internal/intelligence/provider_wave4_rtt.go
CHAINSAW_WAVE4_SUSPICIOUS_REPO_STARSoffWave-4 RTT signal: composite low-stars + fresh repo + dormant commits heuristic (AND semantics; replaces the earlier OR variant). Accepts 1 or <eco>,<eco>.internal/intelligence/provider_wave4_rtt.go
CHAINSAW_WAVE4_MAINTAINER_AGEoffWave-4 signal: maintainer account age. Accepts 1 or <eco>,<eco>. Supported on pypi, cargo, rubygems, packagist/composer, huggingface and docker; npm/yarn/bun only when listed explicitly (or 1), via a GitHub-account heuristic that is not a verified npm-account property and carries a maintainer_age_npm_via_github_heuristic warning. The CHAINSAW_WAVE4_RECOMMENDED preset enables it for pypi, cargo, composer and huggingface.internal/intelligence/premium/provider_wave4_maintainer_age.go
CHAINSAW_WAVE4_RECOMMENDEDoffUmbrella opt-in for the recommended Wave-4 preset. Each per-flag env var still overrides this — CHAINSAW_WAVE4_RECOMMENDED=1 activates the curated default ecosystem list for any flag not explicitly set.internal/intelligence/provider_wave4_rtt.go

A14. Hardening and limit controls

Single-purpose escape hatches and dev/CI gates added by the production-readiness audit-fix sweep. All default OFF; production should leave them unset.

FlagDefaultEffectRead in
CHAINSAW_INVARIANT_PANICoffDev / CI only. When set to a truthy value, the repoRequestCtx single-goroutine invariant in the proxy pipeline panics on violation so the offending callsite is unmissable in tests. Unset (production default) the assertion logs an ERROR and soft-fails — no request-time panic. Never set this in production.internal/server/server_repo_pipeline.go (constant invariantPanicEnvVar)
CHAINSAW_TEST_MALWARE_OVERRIDESunsetTest-only; must be empty in production. Injects synthetic known-malicious entries into the malware index at boot, so QA can live-fire the malware → webhook path. Comma-separated ecosystem:package:version:malware_id[:summary]; real OSV hits take precedence. A non-empty list logs MALWARE TEST OVERRIDES ACTIVE at WARN. Wins over the YAML runtime.malware_test_overrides.core/config/offline.go
GOMEMLIMITunset (derived)Go runtime soft memory limit. When unset, chainsaw-proxy sets it itself to 85% of the container’s cgroup memory limit at startup (Go does not read the cgroup limit on its own), so the GC collects before the kernel OOM-kills the pod; with no cgroup limit nothing changes. An explicit value (standard Go syntax, e.g. 7GiB) always wins. Added after the 2026-09-28 OOMKill.cmd/chainsaw-proxy/init_memlimit.go
CHAINSAW_E2E_LOGIN_EMAIL / CHAINSAW_E2E_LOGIN_PASSWORDunsetOpt-in credentials for the Playwright login spec at ui_new/e2e/login/login-flow.spec.ts. When unset the spec skips. Pair with a stack that has a real password-login user seeded and password auth enabled.ui_new/e2e/login/login-flow.spec.ts

Pre-existing additions also referenced by the audit fix wave: CHAINSAW_ALLOW_PRIVATE_UPSTREAMS (A4 above) is the escape hatch for legitimate on-prem SIEM/webhook destinations whose hostnames resolve to RFC1918 / loopback. Corrected 2026-08-23: this row previously claimed it was used by internal/webhook/ssrf.go::ValidateURL. It is not, and never was — the flag is read only by the dial-time guard in core/httpclient (SafeDialer, and the SIEM CEF raw-TCP SafeNetDial path). The create/update validators do not consult it, so an operator who set it expecting to save an RFC1918 webhook URL got a rejection with no explanation. Use CHAINSAW_ALLOW_CGNAT_UPSTREAMS for tailnet destinations; it is honoured at both layers precisely because a URL you cannot save is a hatch that does not work. Cloud-metadata literals (169.254.169.254, 100.100.100.200, 192.0.0.192, fd00:ec2::254) are refused under every flag combination. CHAINSAW_XREPLICA_SINGLEFLIGHT (A7 above) is the correct env var name for the cross-replica single-flight gate (a stale CHAINSAW_XREPLICAFLIGHT typo in the README was fixed under Y-Y6).


PART B — CONFIGURATION PARAMETERS

B1. Server bootstrap

CLI flags (cmd/chainsaw-proxy/main.go): --config, --user-config, --listen (:8787), --log-level (info), --admin-password, --password-file, --blob-store (data/blobs), --http-timeout (30s), --max-idle-conns (100), --index (data/index.json), --release-age (0), --hook-script, --hook-timeout (5).

YAML: server.listen, server.admin.username (admin), server.tls.{cert_file,key_file,min_version=1.2}, blob_store.root, index.path, exceptions.path, exceptions.age_days.

B2. Database (Postgres)

  • CHAINSAW_DATABASE_URL, CHAINSAW_DATABASE_READ_URL
  • CHAINSAW_BILLY_DATABASE_URL — DSN for the least-privilege billy_ro Postgres role: a second pool where row-level security, not a predicate in a query string, decides which tenant’s rows exist (core/pgstore/rls.go). Billy’s named query tools (query_events, query_policies) prefer this pool and fall back to the ordinary read pool when it is unset — their SQL is server-authored and their org predicate is bound from the session, so they are correctly scoped either way. run_sql, when enabled at all (see CHAINSAW_BILLY_RUN_SQL_DEBUG below), requires it: that tool executes SQL an LLM wrote from attacker-shapeable input, and is refused outright without the boundary underneath it. The remaining Billy tools (lookup_*, check_vulnerabilities, …) keep using the ordinary read pool from CHAINSAW_DATABASE_URL, because billy_ro deliberately cannot see the tables they read. This must be a different Postgres role, not just a different DSN string: RLS binds only a role that is neither the table owner nor a superuser. Startup refuses a DSN resolving to the writer’s role, or to a role with SUPERUSER/BYPASSRLS (pgstore.VerifyBillyRole) — either would silently restore the pre-RLS behaviour. It also refuses two failures that no role attribute reveals: a role covered by any permissive policy other than chainsaw_org_isolation, including through role membership (GRANT chainsaw TO billy_ro hands it the chainsaw_writer_all policy, whose qual is true, while rolsuper and rolbypassrls both still read false), and a Billy-readable table with row-level security switched off — the state the documented incident rollback in docs/OPERATIONS.md#migrations leaves behind. Both are mutation-verified in core/pgstore/rls_test.go. Unset → run_sql is refused (logged, and surfaced to the model as a rejection) and the database-level tenant boundary is not installed; the rest of Billy, including the named query tools, is unaffected. There is deliberately no fallback to CHAINSAW_DATABASE_URL for run_sql. migrate() creates the role NOLOGIN and grants it column-level SELECT on events, policies, repositories, package_metadata; the operator gives it credentials out of band (ALTER ROLE billy_ro WITH LOGIN PASSWORD '…' NOSUPERUSER NOBYPASSRLS). Setup and the pre-rollout check for other readers: MIGRATIONS.md.
  • CHAINSAW_BILLY_RUN_SQL_DEBUG — default unset (off). Set to 1 to offer Billy’s free-form run_sql tool again. Internal debugging only. run_sql lets the model author SQL from a context that includes attacker-controlled package text (a README reaches it as untrusted data), and the SQL guard documents three verified residual bypasses it cannot close, so the only real boundary under it is RLS. The named tools query_events and query_policies cover the questions it was actually used for with typed parameters and a server-authored org predicate, so the default is off — for the authenticated tenant tier as well as the public docs tier. With the flag off the tool is neither offered to the model nor dispatchable if the model emits it anyway, and it is absent from the system prompt. The flag cannot widen the docs-public tier: that tier is gated on an explicit allowlist which run_sql is not in, and never becomes part of.
  • Pool: CHAINSAW_DB_MAX_OPEN_CONNS, _MAX_IDLE_CONNS. CHAINSAW_DB_POOL_SIZE is NOT read by any code — the two names above are the real knobs (parsed in cmd/chainsaw-proxy/init_database.go). Two Prometheus alert annotations in monitoring/alerts.yml still tell on-call to “scale CHAINSAW_DB_POOL_SIZE”; that instruction is a no-op.
  • Lifetimes: CHAINSAW_DB_CONN_MAX_LIFETIME_SECONDS, _CONN_MAX_IDLE_TIME_SECONDS, _STATEMENT_TIMEOUT_SECONDS
  • Read replica: CHAINSAW_DB_READ_MAX_OPEN_CONNS, _MAX_IDLE_CONNS, _CONN_MAX_LIFETIME_SECONDS, _CONN_MAX_IDLE_TIME_SECONDS
  • Compose: POSTGRES_USER (chainsaw), POSTGRES_PASSWORD (chainsaw), POSTGRES_DB (chainsaw)

B3. Blob store

CHAINSAW_BLOBSTORE_TYPE (file|s3). S3: CHAINSAW_S3_ENDPOINT, _REGION, _BUCKET, _ACCESS_KEY, _SECRET_KEY, _SESSION_TOKEN, _USE_PATH_STYLE, _INSECURE, _SSE, _KMS_KEY_ID, _KEY_PREFIX.

CHAINSAW_S3_INSECURE (default off) sends blob-store traffic in plaintext HTTP. When truthy it rewrites the endpoint to http:// — even an endpoint written as https:// — so cached package bytes cross the network unencrypted and can be read or altered in transit. It is for a local MinIO in development. Do not set it in production. chainsaw-proxy logs it at startup under enforcement posture: controls weakened by environment (core/enforcementposture/inventory.go).

B4. Metadata store

CHAINSAW_META_STORE (auto|sidecar|postgres|file).

B5. Message queue

  • CHAINSAW_QUEUE_TYPE (channel|nats|redis), _BUFFER_SIZE, _MAX_DELIVERIES (5), _DLQ_TOPIC
  • NATS: CHAINSAW_NATS_URL, _CREDS, _USER, _PASSWORD, _STREAM_PREFIX, _MAX_BYTES, _MAX_MSGS
  • Redis queue: CHAINSAW_QUEUE_REDIS_URL, _PASSWORD, _MAXLEN

B6. Cache

  • CHAINSAW_CACHE_TYPE (""|redis), CHAINSAW_CACHE_KEY_PREFIX
  • Redis: CHAINSAW_CACHE_REDIS_URL, _USERNAME, _PASSWORD, _POOL_SIZE, _MODE (standalone|sentinel|cluster), _SENTINEL_MASTER, _SENTINEL_ADDRS, _CLUSTER_ADDRS, _TLS
  • Top-level alias set: CHAINSAW_CACHE_REDIS_URL, _PASSWORD, _KEY_PREFIX, _MODE, _POOL_SIZE, _TLS

B7. Leader election

CHAINSAW_LEADER_TYPE (local|redis|postgres), CHAINSAW_LEADER_REDIS_URL, CHAINSAW_LEADER_TTL_SECONDS (1800).

CHAINSAW_RATELIMIT_REDIS_URL — Redis for the shared rate-limit budgets (public Billy chat, run_sql, API buckets), independent of leader election. Unset: the leader’s Redis client is reused when CHAINSAW_LEADER_TYPE=redis, else buckets are in-process (exact at one replica, per-replica beyond it). A malformed URL fails startup rather than silently falling back. The Billy buckets are FailOpen:false, so an unreachable Redis makes public Billy refuse requests. Manifest: dockerized/k8s/redis-ratelimit.yaml.

B8. Auth secrets / keys

  • JWT: CHAINSAW_JWT_SECRET, CHAINSAW_JWT_TTL (24h)
  • CLI key lifetime: CHAINSAW_CLI_KEY_TTL (default 90 days, sliding) — how long a key minted by chainsaw auth login survives without being used. The window slides: every authenticated request pushes the deadline out, so an actively used credential never expires and only abandoned ones lapse. That is what makes a non-never default safe — a fixed TTL would log out every user and every CI job on the same day. Set 0 (or 0s) for the old unbounded behaviour. A malformed or negative value falls back to the default and logs invalid cli key ttl; using default — deliberately not to never, since a typo must not silently return a deployment to unbounded credentials. Applies to newly minted keys only; keys issued before this shipped keep expires_at NULL and are never retro-dated.
  • TLS: CHAINSAW_TLS_CERT_FILE, CHAINSAW_TLS_KEY_FILE, CHAINSAW_TLS_MIN_VERSION (1.2)
  • Encryption envelope chain: CHAINSAW_INTEGRATION_ENCRYPTION_KEY → CHAINSAW_SSO_ENCRYPTION_KEY → CHAINSAW_TOTP_ENCRYPTION_KEY (32-byte hex)
  • TOTP key rotation: CHAINSAW_TOTP_ENCRYPTION_KEY_NEW — the new key during a rotation, read only by chainsaw-proxy -rotate-totp-key (a dry run unless -rotate-totp-key-apply is also given). Every stored secret is re-sealed in one transaction; only after the apply run succeeds do you move the value into CHAINSAW_TOTP_ENCRYPTION_KEY and restart. Swapping the main key without this locks out every 2FA user. Read in internal/server/authapi/totp_rotate.go.
  • Server identity: CHAINSAW_SIGNING_KEY, CHAINSAW_SIGNING_KEY_FINGERPRINT — not implemented. Neither name is read by any Go file, shell script, container manifest or UI file in the repository; setting them has no effect on what the server signs. Policy-bundle signature verification is gated by CHAINSAW_REQUIRE_SIGNED_POLICY_BUNDLE (§B10) and release signing happens in CI, not from server env. Rows kept so the names are not re-documented as live.
  • Test rotation: CHAINSAW_TEST_ENCRYPTION_KEY_A/_B
  • Captcha: TURNSTILE_SITE_KEY, TURNSTILE_SECRET_KEY (+ NEXT_PUBLIC_TURNSTILE_SITE_KEY)

B9. Observability values

  • CHAINSAW_LOG_FORMAT (text|json)
  • OTEL: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME (chain305). CHAINSAW_OTLP_ENDPOINT is documented elsewhere as an alias but is not read by any code — only OTEL_EXPORTER_OTLP_ENDPOINT is (cmd/chainsaw-proxy/main.go, internal/observability/tracing.go).
  • Telemetry: CHAINSAW_TELEMETRY_ENDPOINT

B10. Policy / release / webhooks

  • CHAINSAW_POLICY_EVAL_CACHE_TTL_SECONDS (60), CHAINSAW_POLICY_BUS_DSN
  • CHAINSAW_RELEASE_MIN_AGE_DAYS, release_policy.min_age_days
  • Webhook retry: CHAINSAW_WEBHOOK_MAX_ATTEMPTS (1), _RETRY_BASE_MS (1000), _RETRY_MAX_MS (32000), _BUFFER_SIZE, _FAILURE_THRESHOLD, _ORG_RATE_LIMIT
  • CHAINSAW_WEBHOOK_LEGACY_PERUSER_ROUTING (default off) — restores the legacy install.blocked / install.flagged routing keyed off the client credential’s created_by_user_id. Off, those events fan out org-wide, which is the safer default: a security signal belongs to the org, not to whichever CI user created the credential. Only for operators mid-migration off the per-user path. Wins over YAML runtime.webhook_legacy_peruser_routing. Read in core/config/offline.go.
  • Slack connector (Connectors Phase 2, gated by connectors_slack_oauth): CHAINSAW_SLACK_CLIENT_ID, CHAINSAW_SLACK_CLIENT_SECRET — the Slack OAuth app credentials used by /api/connectors/slack/oauth/{start,callback}. Both must be set for the install flow to start (it returns HTTP 501 otherwise). The client_secret is sent only in the server-side oauth.v2.access POST body and is never logged. The bot token returned by the exchange is encrypted at rest (oauth_token_ciphertext, via CHAINSAW_INTEGRATION_ENCRYPTION_KEY — see §B8). The OAuth callback redirect URI is derived from CHAINSAW_PUBLIC_BASE_URL (§B1) and must be registered in the Slack app’s allowed redirect URLs. The bot-token chat.postMessage client is pinned to slack.com and dials through the SafeDialer (honoring CHAINSAW_ALLOW_PRIVATE_UPSTREAMS, §A4).
  • Bidirectional sync (Connectors Phase 4, gated by connectors_bidirectional): inbound endpoints are POST /api/connectors/slack/events/{connector_id} (authenticated by the Slack v0 request signature — the connector’s Slack signing secret in secret_ciphertext, verified HMAC-SHA256 over v0:{timestamp}:{body} with a 5-min replay window) and POST /api/connectors/jira/events/{connector_id}. Jira classic webhooks carry no HMAC, so the inbound URL is a credential: a per-connector high-entropy token rides in ?token=… (or the X-Chainsaw-Connector-Token header) and is verified against the sha256 stored in the connector config’s inbound_token_hash (constant-time). Treat the configured Jira webhook URL as a secret; rotate by re-issuing the connector’s inbound token. Optionally pin Atlassian’s published egress IP ranges at your edge. Both endpoints resolve org/subject from the stored conversation mapping, never the request body, and dedup every event through connector_inbound_events.
    • Producer fan-out (capture-on-send / sync-on-resolve): when bidirectional is on for an org, a vuln/finding alert that fans out to a Slack-OAuth (bot-token) or Jira connector also records a connector_conversations mapping (the Slack thread_ts / Jira issue key) so a later inbound reply / status webhook / reconcile poll routes back to the right finding or routed violation. When a finding or routed violation is resolved in Chainsaw, a confirmation reply is posted into the captured Slack thread. Both are best-effort and never block the resolution.
    • Jira reconcile poll (finding #12, GA — required because Jira webhook delivery is best-effort): a background worker periodically re-fetches the upstream status for every still-open Jira-issue conversation and reconciles drift through the same idempotent transition shim a webhook uses (no status regression — monotonicity guard; org/subject from the stored mapping, never the polled payload; SafeDialer on the GET /rest/api/3/issue client). Env (ops, not PostHog): CHAINSAW_JIRA_RECONCILE_ENABLED (default off — opt in once a Jira connector is live and connectors_bidirectional is on for the org), CHAINSAW_JIRA_RECONCILE_INTERVAL (15m), CHAINSAW_JIRA_RECONCILE_PAGE_SIZE (100), CHAINSAW_JIRA_RECONCILE_CONCURRENCY (1 — serial, gentle on Jira), CHAINSAW_JIRA_RECONCILE_PER_ROW_PAUSE (0 — optional extra rate-limit between issue polls).

B11. Docker layer scanning sizing + inspector

CHAINSAW_DOCKER_LAYER_SIZE_CAP_BYTES (1 GiB), CHAINSAW_DOCKER_LAYER_SCAN_TIMEOUT_SECONDS (60), CHAINSAW_DOCKER_LAYER_BODY_CAP_BYTES (512 MiB per-layer body buffer). All three are read in internal/hooks/docker_layer_scanner.go. Inspector selector lives in §A5: CHAINSAW_DOCKER_INSPECTOR (eol/oci/both, default both since Wave AF).

Not implemented: CHAINSAW_DOCKER_MALWARE_FEED_URL and CHAINSAW_HUGGINGFACE_MALWARE_FEED_URL are not read by any code. The underlying supplychain.BootstrapConfig.DockerMalwareFeedURL / .HuggingFaceMalwareFeedURL fields exist and are honoured by the syncers, but the only production caller (cmd/chainsaw-proxy/init_server.go) never populates them from the environment. Setting either variable leaves Docker / HuggingFace malware coverage at the embedded seed corpus. internal/server/howtos/tutorials/42-scan-container-image-layers.md still instructs operators to export the Docker one; that instruction is a no-op until the env wiring lands.

YAML hooks.docker_layer.{mode,size_cap_bytes,timeout_seconds} is parsed and defaulted but never consumed — initHooks builds hooks.DockerLayerScannerConfig with only Inspector + Logger, so the scanner falls through to the env vars above on every boot. Use the env vars; hooks.docker_layer.mode: off does not disable layer scanning.

B12. Hidden-Unicode scanner

CHAINSAW_HIDDEN_UNICODE_MAX_FILES (500), CHAINSAW_HIDDEN_UNICODE_MAX_BYTES (50 MiB), CHAINSAW_HIDDEN_UNICODE_THRESHOLD (1), CHAINSAW_HIDDEN_UNICODE_ZEROWIDTH_THRESHOLD (32 — surviving zero-width hits needed before zero-width ALONE moves a verdict; one smuggled ASCII character is ~8 runes). A non-positive or unparseable value falls back to the default. Read in core/hiddenunicode/scan.go.

B13. Provenance trust roots

CHAINSAW_APT_KEYRING, CHAINSAW_RPM_KEYRING, swift.trust_root_bundle_path, swift.identifier_map_path, swift.git_cache_dir, swift.github_org_allowlist (mandatory when swift.github_convention is on — see §A2), swift.swift_registry_url, provenance.disabled_ecosystems (npm/pypi/maven/etc.).

CHAINSAW_PGP_KEYSERVER (default https://keys.openpgp.org) — where Maven PGP verification fetches the signing key named by a signature. The keyserver is the trust anchor: a signature verifies if it was made by whatever key the keyserver returns for that fingerprint; there is no web of trust and no pinned key list. Point it only at a keyserver you trust. Read in core/provenance/pgpverify/verify.go.

npm provenance has no registry key of its own — it follows the npm upstream you already configured. The probe asks <registry>/-/npm/v1/attestations/<pkg>@<ver>, and an attestation only exists on the registry that served the tarball, so asking registry.npmjs.org about a package pulled from a mirror answers about a registry the bytes never came from. Resolution order (config.Config.NPMRegistryURL, threaded through supplychain.BootstrapConfig.NPMRegistryURL into provenance.WithNPMRegistryURL):

  1. repositories[].remote.url of the first enabled, non-hosted format: npm repository;
  2. remotes.npm.url (the per-format default, seeded from the builtin public registry when neither is set).

Repository first because RepositoryConfig.normalize only copies the format default into an empty repo remote and never the reverse. When neither is set the probe falls back to https://registry.npmjs.org — deliberate and explicit, see defaultNPMRegistryURL in core/provenance/npm.go. provenance.offline still short-circuits every ecosystem before any registry is consulted. On the CLI the equivalent is chainsaw verify npm <pkg> <ver> --source-url <registry>; omitting it keeps the same public fallback.

B14. Intelligence refresh tuning

CHAINSAW_INTELLIGENCE_SERVICE (openssf|osv), _REFRESH_INTERVAL (6h), _REFRESH_MAX_STALENESS (7d), _REFRESH_CONCURRENCY (10), _REFRESH_PAGE_SIZE (1000), _REFRESH_ARTIFACT, CHAINSAW_GITHUB_TOKEN. CHAINSAW_TRIVY_DB_PATH is NOT read by any code — the trivy database location comes only from YAML hooks.trivial.db_path (or --trivial-db). The offline-preflight error string in cmd/chainsaw-proxy/offline.go used to name it; that has been corrected. Datasource (per openssf|trivy_db|epss|clamav_db): refresh_interval_seconds, timeout_seconds, jitter_percent (10).

  • CHAINSAW_OSV_BUNDLE_PATH (default /system/osv-bundle.json.gz, the in-image location) — the OSV advisory bundle. The OSV refresher rewrites this file in place, so its parent directory must be writable or the refresher stays dormant. Read in core/intelligence/provider_osv.go.
  • CHAINSAW_OSV_REFRESH_INTERVAL (default 6h; Go duration or bare seconds) — OSV bundle refresh cadence. 0, 0s, off, disabled, false or no disables the refresher; CHAINSAW_OFFLINE also disables it. Read in core/intelligence/osv/refresher.go.
  • CHAINSAW_TRANSITIVE_DEPTH (default 5, clamped to 10) — how many levels of cached dependencies the transitive-risk rollup walks. A non-positive or unparseable value falls back to 5. Read in core/intelligence/provider_transitiverisk.go.

B15. Reaper

CHAINSAW_REAPER_INTERVAL (24h), CHAINSAW_REAPER_MIN_AGE_HOURS (24).

B16. Upstream HTTP

CHAINSAW_UPSTREAM_MAX_RETRIES (3), CHAINSAW_UPSTREAM_MAX_BACKOFF (30s), CHAINSAW_UPSTREAM_NEGATIVE_CACHE_TTL (30s, capped at 60s; Go duration or bare seconds — how long an upstream error response is cached; read once per process). Per-host: CHAINSAW_UPSTREAM_LIMIT_<HOST>. Observed: _PYPI_ORG, _REGISTRY_NPMJS_ORG, _RUBYGEMS_ORG, _CRATES_IO, _PACKAGIST_ORG, _SEARCH_MAVEN_ORG, _HUGGINGFACE_CO, _HUGOVK_DEV, _AZURESEARCH_USNC_NUGET_ORG. YAML: http_client.{timeout_seconds=60, tls_insecure, max_idle_conns=64}.

B17. ClamAV

clamav.socket_path (/tmp/clamd.socket), clamav.timeout_seconds (60), clamav.max_stream_bytes (512 MiB), CHAINSAW_CLAMAV_SOCKET_ALLOWED_PREFIXES, CHAINSAW_CLAMAV_DB_DIR (a relocated freshclam DatabaseDirectory, probed first by the signature-freshness check; defaults probed after it are /data/clamav, /var/lib/clamav, /var/clamav, /usr/local/share/clamav).

B18. Email / LLM / billing

  • Email: POSTMARK_SERVER_TOKEN, POSTMARK_FROM_EMAIL, POSTMARK_MESSAGE_STREAM (outbound)
  • Postmark webhook basic auth: POSTMARK_WEBHOOK_USER, POSTMARK_WEBHOOK_PASSWORD — credentials Postmark must present on /api/webhooks/postmark. Unset, or only one set, and every request is rejected as unconfigured. A weak password (under 24 characters, equal to the user, or a stock value) makes the server refuse to register the route at boot. Read in cmd/chainsaw-proxy/init_server.go and internal/server/postmark_webhook.go.
  • LLM: OPENROUTER_API_KEY, OPENROUTER_MODEL, BILLY_MODEL (google/gemini-2.5-flash), BILLY_FALLBACK_MODELS (comma-separated fallback list — empty disables failover; non-empty wires the FailoverCompleter with a per-model circuit breaker, see docs/ARCHITECTURE.md#billy-safety-matrix)
  • Billy public docs tier: BILLY_DOCS_CORPUS_PATH — the published llms-full.txt the public docs_search tool retrieves from; it must be a PUBLISHED artifact, never internal source. Unset, the corpus is empty. BILLY_PUBLIC_TOKEN_KEY — HMAC key for public-tier session tokens; unset, one is derived from the JWT secret with a domain-separation label, and with neither, minting is disabled (fails closed). Read in internal/billy/public_tier.go and internal/server/public_billy_token.go.
  • Paddle: PADDLE_ENV, PADDLE_API_KEY, PADDLE_CLIENT_TOKEN, PADDLE_WEBHOOK_SECRET, PADDLE_PRICE_{PRO,UNLIMITED}_{MONTHLY,ANNUAL}
  • Signup gating: CHAINSAW_BLOCKED_EMAIL_DOMAINS, CHAINSAW_BLOCK_CONTACT
  • Analytics / feature flags (server-side; the CLI and UI never hold these keys): POSTHOG_API_KEY (project ingestion key — with it unset the flag client returns a nil-safe sentinel and every IsEnabled call falls back to the default baked into its call site, so the server runs normally without PostHog), POSTHOG_HOST (self-hosted endpoint), POSTHOG_PERSONAL_API_KEY (opt-in local flag evaluation — the SDK polls an in-memory map every ~30s instead of making one decide() HTTP call per check, which is what makes flag lookups cheap on hot paths), POSTHOG_REVERSE_ETL_KEY + POSTHOG_PROJECT_ID (both required together for the reverse-ETL export; the exporter errors out if either is missing)
  • Procurement-kit (GTM W5, lead-form delivery): POSTMARK_PROCUREMENT_KIT_TEMPLATE_ID (Postmark template ID for the kit-delivery email; unset → handler falls back to inline HTML body so the surface is exercisable in dev), CHAINSAW_PROCUREMENT_KIT_URL (overrides the embedded download link; defaults to <base>/procurement/chainsaw-procurement-kit-latest.zip).
  • Trust packet: CHAINSAW_TRUST_PACKET_NOTIFY_EMAIL — optional internal mailbox that gets a one-line heads-up per /security trust-packet request. Unset sends no internal mail; the leads table is then the only record. Read in internal/server/leads_trust_packet.go.

B19. Misc server

CHAINSAW_DEFAULT_DOCKER_REPO, CHAINSAW_DEFAULT_DOCKER_ORG, CHAINSAW_WEB_UI_URL, CHAINSAW_HOWTOS_DIR, CHAINSAW_API_BASE_URL, CHAINSAW_DOCS_URL, geoip.db_path, hooks.{request_script,timeout_seconds=5,docker_layer.*}.

ValueDefaultEffect
CHAINSAW_GEOIP_DB_PATH/data/GeoIP2-Country.mmdb when that file existsOn-disk MaxMind country database, probed only when the YAML geoip.db_path is unset. With neither set and no file at the default path, country resolution falls back to the CF-IPCountry header plus the bundled test database.
CHAINSAW_SCAN_STORE(unset = in-process memory)Set to postgres to persist lockfile-scan jobs in the database instead of process memory — required for multi-replica deployments, where a job minted on one replica must be pollable from another. Requires a configured store; if the Postgres store fails to construct, the server logs a WARN and falls back to memory so the endpoint stays available.
CHAINSAW_SCAN_HMAC_KEY(per-process random)Key used to sign anonymous scan job IDs, as hex or base64; anything decoding to fewer than 32 bytes is rejected with a WARN. Set this on every HA deployment. Without it each replica generates its own key, so a job ID minted on replica A returns 404 from replica B. Single-replica deployments can leave it unset.
CHAINSAW_SEED_CONFIG—Optional seed config whose remote URLs become the hardening bundle’s firewall deny list. Unset, the list is the upstream URL of every repository the requesting org proxies. When no host can be found, or a set file cannot be read, the bundle carries OMITTED.txt and an X-Chainsaw-Bundle-Omitted: network header instead of silently lacking the firewall files.
CHAINSAW_POLICY_BUNDLE—Path to the signed Rego policy bundle (directory or .rego file) for the custom-rule DSL. Unset leaves the DSL path disabled and the previously-loaded engine in place. A non-existent path logs a WARN and keeps the previous engine. Signature verification runs before compilation, so a .rego dropped into the bundle directory cannot be compiled in the window between hash and load — see docs/ENFORCEMENT_AND_POLICY.md#policy-signed-bundles and CHAINSAW_OPA_SKIP_VERIFY in A4. Also read by the K8s admission webhook as the default for --bundle (B27c).
CHAINSAW_DATACENTER_CIDRS_FILE—Newline-delimited CIDR file merged on top of the embedded cloud/datacenter ranges at startup. Feeds only the telemetry-ingest anomaly signal (a surge of installs from cloud ranges); the request IP is never stored. Read in internal/netclass/netclass.go.
CHAINSAW_TRIVIAL_PACKAGE_LOC_THRESHOLD10Lines-of-code bound below which the trivial-package signal fires. Valid range 1–1000; anything else logs a warning and uses the default. Read in internal/intelligence/premium/provider_wave4_artifact.go.
CHAINSAW_TOO_MANY_FILES_THRESHOLD5000Archive entry count above which the too-many-files signal fires. Valid range 100–100000; anything else logs a warning and uses the default. Same file as above.

B19a. Source-forge API tokens (repo-activity intelligence)

Optional. The repo-activity signal reads commit/contributor statistics from the forge that hosts a package’s source. Each token is used only to raise that forge’s anonymous rate limit — unset means anonymous requests, which work until the limit is hit and then degrade the signal rather than failing the scan. Grant read-only scopes; none of these need write access.

ValueForgeRead in
GITHUB_TOKENGitHubcore/malware/ghsa.go (GHSA advisory queries) plus the repo-activity provider
CHAINSAW_GITLAB_TOKENGitLabinternal/intelligence/premium/provider_wave4_rtt.go
CHAINSAW_BITBUCKET_TOKENBitbucketinternal/intelligence/premium/provider_wave4_rtt.go
CHAINSAW_CODEBERG_TOKENCodeberginternal/intelligence/premium/provider_wave4_rtt.go

B20. UI runtime endpoints (server-side env)

CHAINSAW_API_BASE_URL, _INTERNAL_API_BASE_URL, CHAINSAW_API_ORIGIN, CHAINSAW_API_BASEPATH, _UI_BASEPATH, NEXT_APP_BASEPATH, NEXT_APP_HOSTNAME, CHAINSAW_UI_DIR, CHAINSAW_API_AUTH_HEADER, _API_USERNAME, _API_PASSWORD, _API_BASIC_AUTH. CHAINSAW_API_SESSION_COOKIE is not implemented — no Go, shell or UI file reads it; the dashboard’s session cookie name is not configurable. Name kept here so it is not re-documented as live.

The Go server reads four of these too (client-guide URLs, hardening bundle callback URLs, the request base path), and falls back to the build-time spelling when the plain name is unset: CHAINSAW_REPO_BASE_URL → NEXT_PUBLIC_CHAINSAW_REPO_BASE_URL, CHAINSAW_API_BASE_URL → NEXT_PUBLIC_CHAINSAW_API_BASE_URL, CHAINSAW_API_BASEPATH → NEXT_PUBLIC_CHAINSAW_API_BASEPATH, CHAINSAW_API_ORIGIN → NEXT_PUBLIC_CHAINSAW_API_ORIGIN (firstEnv in core/config/client_guides.go, internal/server/server_core.go, internal/server/hardeningapi/bundle.go).

B21. UI runtime endpoints (client-side, baked at build)

NEXT_PUBLIC_CHAINSAW_API_BASE_URL, _API_BASEPATH, _BASEPATH, _API_AUTH_HEADER, _AUDIT_API_BASE_URL, NEXT_PUBLIC_POSTHOG_KEY, NEXT_PUBLIC_POSTHOG_HOST.

B27a. Background-worker tuning

  • Feedback tuner: CHAINSAW_FEEDBACK_TUNER_INTERVAL (Go duration, default 1h) — cmd/chainsaw-proxy/init_feedback_tuner.go
  • Ownership SLA: CHAINSAW_OWNERSHIP_SLA_INTERVAL (Go duration, default 1m — from ownershipsla.DefaultInterval) — cmd/chainsaw-proxy/init_ownership_sla.go
  • Bypass-history snapshotter: CHAINSAW_BYPASS_HISTORY_INTERVAL (Go duration, default 1h) — cmd/chainsaw-proxy/init_bypass_history.go
  • Datacleanup loop:
    • CHAINSAW_DATACLEANUP_INTERVAL (Go duration, default 6h)
    • CHAINSAW_FEEDBACK_RETENTION_DAYS (int, default 90) — prunes feedback_events rows older than N days
    • CHAINSAW_NONCE_RETENTION_EXPIRED_DAYS (int, default 7) — prunes hardening_approval_nonces rows past expires_at and never consumed
    • CHAINSAW_NONCE_RETENTION_CONSUMED_DAYS (int, default 30) — prunes consumed hardening_approval_nonces rows; longer window because consumed rows are audit-relevant
    • CHAINSAW_GUARD_BLOCK_RETENTION_DAYS (int, default 90) — prunes guard_block_events on blocked_at and guard_installs on last_seen_at. One knob for both deliberately: a block insert advances its install’s last_seen_at in the same transaction, so a shorter block window can never orphan a live block and a divergent pair could
    • All read in cmd/chainsaw-proxy/init_datacleanup.go (defaults sourced from internal/datacleanup/cleanup.go)
  • Dormancy flagger: CHAINSAW_DORMANCY_INTERVAL (Go duration, default 24h), CHAINSAW_DORMANCY_THRESHOLD_DAYS (int, default 14; non-positive falls back to 14) — cmd/chainsaw-proxy/init_dormancy.go
  • Exception reminders: CHAINSAW_EXCEPTION_REMINDER_INTERVAL (Go duration, default 1h) — cmd/chainsaw-proxy/init_exception_reminders.go
  • Stale-finding reminders: CHAINSAW_STALE_FINDING_REMINDERS_INTERVAL (Go duration, default 1h) — cmd/chainsaw-proxy/init_stale_finding_reminders.go
  • SBOM snapshotter: CHAINSAW_SBOM_SNAPSHOT_INTERVAL (Go duration, default 24h) — cmd/chainsaw-proxy/init_sbom_snapshotter.go
  • Security digest: CHAINSAW_SECURITY_DIGEST_INTERVAL (Go duration, default 6h, the periodic backstop — a feed refresh normally triggers a pass immediately) — cmd/chainsaw-proxy/init_security_digest.go
  • Flywheel capture: CHAINSAW_FLYWHEEL_INTERVAL (Go duration, default 1h) — cmd/chainsaw-proxy/init_flywheel.go
  • Inventory drift detector: CHAINSAW_INVENTORY_DRIFT_INTERVAL (Go duration, default 60s) — how often the package_versions write count is compared against the success counter; a >10% divergence logs inventory drift detected. Overridable for testing; leave the default in production. cmd/chainsaw-proxy/init_server.go
  • Billy ledger retention: BILLY_LEDGER_RETENTION_DAYS (int, default 30), BILLY_LEDGER_RETENTION_INTERVAL (Go duration, default 6h) — cmd/chainsaw-proxy/init_billy_ledger_retention.go
  • Every Go-duration knob above falls back to its default when unset, unparseable or non-positive.

B27b. Hardening / phone-home values

  • CHAINSAW_PUBLIC_URL — the API base (including any ingress mount, e.g. https://chain305.com/chainproxy) stamped into hardening bundle scripts and ack-callback URLs. Unset, falls back to CHAINSAW_API_BASE_URL with /api/v1 removed, then to the request host plus its X-Forwarded-Prefix mount. Read in internal/server/hardeningapi/bundle.go.
  • CHAINSAW_WEBHOOK_HMAC_SECRET — shared HMAC secret for shadow-mode admission POSTs and the K8s bundle phone-home channel. Required when phone-home is configured. Read in internal/server/admission_shadow_api.go, enforcement/k8s-admission/cmd/server/main.go.
  • CHAINSAW_API_BASE_URL / CHAINSAW_REPO_BASE_URL — existing knobs, also consumed by hardening ack-callback URL rendering and client guides.
  • POD_NAME — Kubernetes downward-API value used as leader-election identity by the admission webhook. Read in enforcement/k8s-admission/cmd/server/leader.go.

B27c. K8s admission webhook CLI flags

Read in enforcement/k8s-admission/cmd/server/main.go. Each flag mirrors a CHAINSAW_* env var as its default.

FlagEnv fallbackEffect
--tls-certCHAINSAW_ADMISSION_TLS_CERTPath to the webhook server certificate. Kubernetes only talks TLS to admission webhooks, so this and --tls-key are effectively mandatory.
--tls-keyCHAINSAW_ADMISSION_TLS_KEYPath to the webhook server private key.
--bundleCHAINSAW_POLICY_BUNDLEPath to the chainsaw.policy Rego bundle (directory or .rego file). Same variable the proxy reads (B19).
--correlation-endpointCHAINSAW_CORRELATION_ENDPOINTWhere admission decisions are POSTed for deploy-correlation. Paired with the YAML correlation.enabled block on the server side.
--correlation-tokenCHAINSAW_CORRELATION_TOKENBearer token for that endpoint.
--shadow-mode(bool, default false)Enable W6 admission shadow-mode telemetry — POST decisions to the control plane in addition to local admit/deny.
--control-plane-urlCHAINSAW_CONTROL_PLANE_URLBase URL of the chainsaw control plane; the shadow + phone-home paths are appended automatically. Required with --shadow-mode.
--webhook-hmac-secretCHAINSAW_WEBHOOK_HMAC_SECRETHMAC key used to sign shadow-mode and phone-home POSTs.
--org-idCHAINSAW_ORG_IDChainsaw org_id stamped on every shadow-mode report. Required with --shadow-mode.
--cluster-idCHAINSAW_CLUSTER_IDCluster identifier stamped on shadow-mode + phone-home reports.
--phone-home-urlCHAINSAW_PHONE_HOME_URLEndpoint for the K8s bundle phone-home channel ({bundle_id, cluster, applied_at}).
--phone-home-bundle-idCHAINSAW_PHONE_HOME_BUNDLE_IDBundle id (sha256) reported by phone-home.
--phone-home-bundle-id-pathCHAINSAW_PHONE_HOME_BUNDLE_ID_PATHPath to a file containing the bundle id (e.g. mounted ConfigMap). Fallback when --phone-home-bundle-id is empty.
--leader-election-modeCHAINSAW_LEADER_ELECTION_MODElease (client-go), env (legacy CHAINSAW_K8S_PHONE_HOME_LEADER deploy-time gate), or none.
CHAINSAW_K8S_PHONE_HOME_LEADER (env-only)—Legacy deploy-time leader gate, still honoured under --leader-election-mode=env; superseded by client-go lease. Read in enforcement/k8s-admission/cmd/server/phone_home.go.

B27d. CLI flags

Global output-control flags (CLI UX wave)

Registered on rootCmd.PersistentFlags() in core/cli/root.go — available on every chainsaw subcommand. Purely additive; existing --json/--no-color behaviour is unchanged. The output helpers that read them live in core/cli/output.go.

FlagShortTypeDefaultEnvNotes
--quiet-qboolfalseCHAINSAW_QUIETSuppress progress/chatter only. Never suppresses a block reason and never changes an exit code. Precedence: flag > env (viper-bound).
--verbose-vboolfalseCHAINSAW_VERBOSEEmit extra diagnostic detail to stderr. Precedence: flag > env (viper-bound).
--format—stringtable—Result format: table or json globally. --json is sugar for --format=json; resolveFormat() reconciles the two (case-insensitive). Individual commands extend the value set: chainsaw scan --format sarif emits a SARIF 2.1.0 log; chainsaw sbom export --format spdx emits SPDX 2.3 (typically paired with --output).
--output-ostring""—Write the result to this file (os.Create) instead of stdout. Logs/progress stay on stderr; falls back to stdout on open error. RESULTS sink only.
--json—boolfalse—Unchanged. Now an alias resolving to --format=json; byte-compatible output.
--no-color—boolfalseNO_COLORUnchanged. Also disabled when TERM=dumb or stdout is not a terminal.

Guard safety. -v, -o, and -q are now bound at the persistent-flag level. Every chainsaw global flag is registered in chainsawGlobalBoolFlags / chainsawGlobalValueFlags (core/cli/guard_install.go) so a guard subcommand (which runs with DisableFlagParsing) can never leak a chainsaw global to the wrapped package manager. The guard_globalflags_test.go regression test enforces this.

For the related process exit codes (block vs. operational error, and the 1 → 2 behaviour change for operational errors), see docs/ERRORS.md#errors-readme → CLI process exit codes.

  • chainsaw doctor --bundle-id=<sha256> — W11 MDM phone-home channel. When supplied with --attest, the attest POST body includes bundle_id; the proxy stamps applied_at on the matching hardening_bundles row. MDM-rendered install scripts pre-fill this from the hardening wizard (/admin/hardening). Defined in core/cli/doctor.go.

Removed in the open-core split: the chainsaw harden CLI family (incl. lint-firewall) was removed; hardening-bundle generation, approval, and emission now live behind the admin wizard at /admin/hardening and the /api/hardening/* API. The generated firewall allowlist is correct by construction (the emitter injects RequiredIntelHosts()), so the standalone firewall linter is obsoleted by regeneration. See docs/RUNBOOKS.md#runbooks-hardened-enforcement.

B27e. Webhook destination columns (webhooks table)

Added by the post-merge migration in core/pgstore/store.go (around L2088); both columns gracefully degrade to legacy behaviour when missing.

  • format TEXT NOT NULL DEFAULT 'generic' — body shape: generic, slack, teams. Selects the renderer used by the webhook outbox.
  • topic TEXT NOT NULL DEFAULT 'all' — coarse subscription filter: all (every event) or a specific topic key (today: hardening_approvals). ListEnabledForTopicAndFormats filters on (topic = 'all' OR topic = ?) and format IN (...).

B27f. Auth-surface per-IP rate limits

Hard-coded per-IP token-bucket limits on the unauthenticated auth surface — no env tuning today; values live in internal/server/auth_ratelimit.go. Documented here so operators can size CDN/WAF rules around them.

EndpointBurstSustainedRead in
POST /api/login1060 / hourloginIPLimiter
POST /api/auth/signup530 / hoursignupIPLimiter
POST /api/auth/forgot-password320 / hourforgotPasswordIPLimiter

On deny the response is HTTP 429 with a Retry-After: <seconds> header (seconds rounded up to ≥1). IP resolution mirrors the anonymous-scan gate (X-Forwarded-For aware via requestIP, falls back to anonClientID). Cold per-IP buckets are evicted by sweepAuthIPLimiters from StartScanReaper (2h TTL). Turnstile (when configured) runs INSIDE the rate-limit envelope so a CAPTCHA solve still hits the 429 ceiling.

B27g. Audit-export ceiling

GET /api/audit/logs?export=true rejects requests when the estimated row count exceeds the in-memory ceiling. Returns HTTP 413 with error code CHW-4519 CodeAuditExportTooLarge. The dashboard audit view UI is unaffected (paginated reads).

ConstantValueRead in
auditExportRowCeiling50000internal/server/dashboard.go

A streaming export replacement is tracked as a TODO — see the comment block immediately above the constant.

B27h. Random token entropy

generateRandomToken (internal/server/auth.go) is the single entropy source for session tokens, JWT signing material, SSO state/nonce/binding, magic-link tokens, and pending-auth tokens. The function reads 256 bits from crypto/rand and base64url-encodes; on crypto/rand failure it returns ("", err) — there is intentionally no fallback path. Bootstrap callers (loadOrGenerateJWTSecret, SSO encryption-key materialisation) treat the error as fatal and refuse to start the process; per-request callers return HTTP 500. The previous wall-clock fallback (base64(time.Now().UnixNano())) was removed and must not be reintroduced.

B27. CLI / installer (client-side env)

CHAINSAW_CONFIG, CHAINSAW_CONFIG_HOME, CHAINSAW_DATA_DIR, CHAINSAW_BIN, CHAINSAW_BIN_SOURCE, CHAINSAW_INSTALL_DIR, CHAINSAW_INSTALLER_URL, CHAINSAW_DEFAULT_SERVER, CHAINSAW_SERVER (alias CHAINSAW_SERVER_URL), CHAINSAW_TOKEN, CHAINSAW_REPOSITORY / _REPO / _REPO_BASE / _REPO_BASE_URL / _REPOSITORY_URL, CHAINSAW_PACKAGE, _PACKAGE_ID, _VERSION, _URL, _METHOD, _STATUS_CODE, _HOST, _LOGICAL_PATH, _FROM_CACHE, _FORMAT, _FLAGS, _ARGS, _PID, _CLIENT_TYPE, _UI_HOSTNAME, CHAINSAW_REPLICAS.

Install guard (chainsaw npm/pip/go, core/cli/guard_*.go): CHAINSAW_GUARD_DB — path to the local known-malicious cache the guard merges on top of its embedded floor; written by chainsaw guard update (the one opt-in networked command). Default: <user-cache>/chainsaw/known_malicious.json. The guard’s default path is offline and sends nothing. Its other env:

  • CHAINSAW_GUARD_ALLOWLIST — path to the local waiver file. Default: guard_allowlist.json in the CLI config dir. Every coordinate in that file is let through, so whoever controls this path controls what the guard waives.
  • CHAINSAW_GUARD_ARTIFACT_DIR — directory of pre-staged archives (<dir>/<ecosystem>/<name>-<version>.<ext>) for offline behavioural analysis. Unset: no pre-staged analysis.
  • CHAINSAW_GUARD_DEEP — truthy fetches a pinned npm/cargo archive from the registry to analyse it BEFORE install. This is a network egress, announced once on stderr and recorded in guard_state.json; CHAINSAW_OFFLINE always wins over it.
  • CHAINSAW_GUARD_NPM_REGISTRY (default https://registry.npmjs.org), CHAINSAW_GUARD_CARGO_BASE (default https://static.crates.io) — where deep mode fetches from. Set them to the registry your package manager actually installs from, or the guard analyses a different copy than the one that gets installed.

chainsaw doctor --upgrade-check / --fix (core/doctor/doctor.go) also reads CHAINSAW_FLAGS and CHAINSAW_ARGS (scanned for removed CLI flags). Its TLS check resolves the cert and key the way the server does: CHAINSAW_TLS_CERT_FILE / CHAINSAW_TLS_KEY_FILE (§B8) win, per field, over YAML server.tls.{cert_file,key_file} from --config (or CHAINSAW_CONFIG). It cannot see a path stored only in the database settings table, and says so when it finds none.

Cargo credentials (core/cli/cargo_credentials.go). lookupCargoCredentials walks four sources and takes the first hit, in this order:

OrderSourceNotes
1CHAINSAW_CARGO_CREDENTIALSclient_id:client_secret for the cargo registry, encoded into the Basic header cargo’s credential-provider protocol expects.
2CHAINSAW_CLIENT_CREDENTIALSSame shape; the generic fallback when one credential pair serves more than cargo.
3OS keyringEntry chainsaw/cargo-credentials@<server>, written by chainsaw cargo-credentials store. Works before chainsaw auth login — the account degrades to cargo-credentials when no server is configured yet.
4cargo_credentials: in the CLI config fileHand-authored plaintext-on-disk fallback. Never written by the CLI, and deliberately preserved when the CLI rewrites its config.

Error messages name which of the four a malformed credential came from.

B27i. Third-party and OS environment variables the CLI reads

Not Chainsaw configuration — these are variables the CLI consumes because they already govern something it has to locate or respect. Listed so the set is complete and so nobody re-adds a Chainsaw-prefixed alias for a variable the ecosystem already owns.

ValueWhy the CLI reads it
PATHchainsaw doctor resolves package-manager binaries and checks whether the guard shims shadow them.
SHELLchainsaw guard init picks the shim syntax for the user’s shell.
TERM, NO_COLORTTY and color detection. NO_COLOR is honoured when present at any value (the no-color.org convention) and deliberately beats an explicit --no-color=false.
WT_SESSION, TERM_PROGRAM, WSL_DISTRO_NAME, MSYSTEMWindows console detection for the Unicode decision (core/cli/unicode_decide.go): Windows Terminal, VS Code’s terminal, WSL and MSYS2/Git Bash all render Unicode, so any of them keeps glyphs on.
CISuppresses interactive prompts and TTY-only nudges on build agents.
USER, USERNAMEBest-effort operator label in chainsaw doctor --strict output.
DISPLAY, WAYLAND_DISPLAYBrowser-login availability check: no display server means no browser to open, so chainsaw auth login routes to the device-code flow instead of hanging.
XDG_CONFIG_HOME, APPDATA, AppData, ProgramDataPlatform config roots for the CLI’s own config file and for locating package-manager config. Windows env lookups are case-insensitive at the OS level but Go’s os.Getenv is not, so both APPDATA and AppData are probed.
NPM_CONFIG_USERCONFIG, npm_config_cacheLocates the user’s .npmrc and npm’s cache directory so doctor inspects the files npm actually reads. npm’s own env convention is lowercase.
PIP_CONFIG_FILE, PIP_CACHE_DIR, VIRTUAL_ENVSame for pip: config location, cache location, and whether a virtualenv is active.
CARGO_HOMELocates cargo’s config and credential store.
M2_HOME, MAVEN_HOMELocates the Maven install and settings.xml.
GRADLE_HOME, GRADLE_USER_HOMELocates Gradle’s init scripts and cache.
GOENVLocates the Go environment file the go guard has to edit.
KUBERNETES_SERVICE_HOSTIn-cluster detection for the admission webhook — present only when running inside a pod.

B28. HuggingFace proxy

Built-in seeded repository (builtinRepositories[].huggingface in core/config/config.go). Defaults map to upstream huggingface.co with AnonymousAccess: true (HF public models don’t require credentials), 600s negative-cache TTL, and a 120s upstream timeout.

KnobValueSource
Upstream URLhttps://huggingface.codefaultUpstreams.huggingface.URL
Upstream timeout120sdefaultUpstreams.huggingface.TimeoutSeconds
Negative-cache TTL600sbuiltinRepositories.huggingface.Cache.NegativeTTLSeconds
Anonymous fetchenabledbuiltinRepositories.huggingface.AnonymousAccess
Per-host upstream concurrencyCHAINSAW_UPSTREAM_LIMIT_HUGGINGFACE_COsee B16
LFS object size cap (push)2 GiBhard-coded maxLFS in internal/server/huggingface_handlers.go

Error codes for the HF surface live at internal/errcodes/registry_huggingface.go (CHW-8301 … CHW-8313) — CHW-8307 repo-name invalid, CHW-8308 LFS SHA missing, CHW-8309 LFS object not found, CHW-8310 LFS path invalid, CHW-8311 LFS object too large, CHW-8312 LFS OID mismatch, CHW-8313 LFS size mismatch. See docs/ERRORS.md for human-readable descriptions.

ML-model intelligence (pickle-opcode scan, AI-artifact provider, capability detection) runs on HF artifacts automatically via internal/intelligence/premium/provider_aiartifact.go — no separate enable flag. Per-ecosystem coverage: run chainsaw policy preflight against your server (the same matrix is POLICY_PROXY_MATRIX.md in the source repo).

B30. Credential env-var vocabulary

Chainsaw issues two unrelated kinds of credential, and one variable name — CHAINSAW_TOKEN — is used for both. Nothing here is being renamed: the forms below are different shapes, not synonyms, so neither converts into the other, and every rename breaks a customer already scripted against the old name. This table is the map instead (P8-30, ruled 2026-09-02).

Verified against the code that consumes each value, not against the surfaces that teach it.

The two credentials

Client credential (package registry)Management-API key (control plane)
What it opens/repository/@<slug>/<ecosystem>/… — package pulls and publishes/api/… — the dashboard’s own REST API
Value shape<client_id>:<client_secret> — plaintext, colon-separated, never base64c305_pa_<12 base32>_<43 base64url> (personal) or c305_ag_… (agent); alternatively a session JWT
Minted atAccess → Client CredentialsAccess → API keys (apikeys.Generate, internal/apikeys/apikeys.go)
Server-side parsesplitTokenCredential (internal/server/server_clients.go) — strings.SplitN(token, ":", 2), reached via extractClientCredentials → resolveClientCredentialsapikeys.IsAPIKey → AuthenticateAPIKey (internal/server/authcore/auth.go), gated on the literal c305_ prefix
Accepted asAuthorization: Basic, Authorization: Bearer <id>:<secret>, URL userinfo, X-NuGet-ApiKeyAuthorization: Bearer only

They are disjoint in both directions, and this is enforced by the parsers, not by convention. A c305_… key contains no colon, so splitTokenCredential returns ok=false and a package fetch 401s. An <id>:<secret> pair has no c305_ prefix, so IsAPIKey is false, the request falls through to the JWT branch, and signature parsing fails. Pasting either into the other’s slot produces a 401 with no hint about which credential was wrong.

Because the split is SplitN(…, ":", 2), a client_id may not contain a colon; a client_secret may. Everything after the first colon is the secret.

Variable-by-variable

VariableWhich credentialExact valueWho reads itWhich surface teaches it
CHAINSAW_TOKEN (package-manager surfaces)client credential<client_id>:<client_secret>Not Chainsaw. The shell or the package manager expands it — npm/Yarn/Bun expand ${CHAINSAW_TOKEN} inside .npmrc themselves — and the proxy sees only the expanded value on the wireclient_configuration_guide prose in configs/seed.yaml and its dockerized/config.yaml twin (44 occurrences); the npm placeholder branch of the config-snippet API
CHAINSAW_TOKEN (CLI + how-to surfaces)management-API keyc305_pa_… / c305_ag_…, or a session JWTcore/cli/root.go cfgToken (viper AutomaticEnv, CHAINSAW prefix), --token outranks it; postAttestation in core/cli/doctor_strict.go reads it directly19 of the how-to tutorials, all of them sending Authorization: Bearer $CHAINSAW_TOKEN at an /api/… URL; the token: input of enforcement/github-actions/action.yml
CHAINSAW_CLIENT_ID + CHAINSAW_CLIENT_SECRETclient credentialtwo separate secretsNot Chainsaw. The CI job joins them at the point of use: "$CHAINSAW_CLIENT_ID:$CHAINSAW_CLIENT_SECRET"9 how-to tutorials (the CI/CD family 21–26, plus 18, 43, 54); chainsaw-landing/src/pages/integrations.astro
CHAINSAW_CLIENT_SECRET (config-snippet API)client credentialnot an env var at all — a literal placeholder stringNothing. placeholderClientSecret (internal/server/server_configsnippets.go) is substituted into the secret slot of the rendered pip.conf / settings.xml / nuget.config for the reader to hand-replace; a secret is unrecoverable after issue, so a revisited credential always renders itGET /api/clients/{client_id}/config-snippet
HF_TOKENclient credential<client_id>:<client_secret>huggingface_hub, which sends Authorization: Bearer $HF_TOKEN. A third-party variable Chainsaw does not ownthe huggingface client_configuration_guide in configs/seed.yaml
CHAINSAW_CARGO_CREDENTIALS, CHAINSAW_CLIENT_CREDENTIALSclient credential<client_id>:<client_secret>genuinely read, via os.Getenv, by lookupCargoCredentials (core/cli/cargo_credentials.go) — see B27 for the four-source precedencechainsaw cargo-credentials error text

Converting between the forms

  • Split pair → combined: "$CHAINSAW_CLIENT_ID:$CHAINSAW_CLIENT_SECRET". Plaintext, one colon, no quoting beyond the shell’s own. Never base64-encode it: the proxy splits a Bearer token on a literal :, and base64 output has none, so an encoded value fails with E401 client credentials required on the first request. Authorization: Basic is the one place base64 is correct, and the tool builds that header itself.
  • Combined → split pair: split on the first colon.
  • Either → a management-API key: impossible. Mint one at Access → API keys. There is no derivation, and no endpoint accepts both.

Present-but-empty means anonymous

An empty --token "" or an exported-but-empty CHAINSAW_TOKEN= is a decision, not an omission: the CLI treats it as “no credential” and does not fall back to the keyring / file credential store for that server. It used to — viper folds an empty value to unset, so a job whose secret was never populated, or an operator who cleared the variable to run anonymously, was silently authenticated with whatever the machine had stored. Anonymous-capable commands (doctor, status, scan, intel, guard status, the GitHub Action’s offline path, which exports CHAINSAW_TOKEN: ${{ inputs.token }} with default: "") keep running anonymously; a command that needs a credential exits 3 (ExitConfigAuth) with --token / CHAINSAW_TOKEN is set but empty; refusing to fall back to the stored credential (unset it, or pass a value). A non-empty --token still outranks both the env var and the store. Whitespace-only values count as empty. A YAML token: "" key is not an override (it is migrated to the credential store on first run). Pinned by core/cli/token_empty_test.go.

Why both vocabularies stay

CHAINSAW_TOKEN and CHAINSAW_CLIENT_ID/_SECRET carry different shapes, so standardising on one is a value change, not a rename — every pipeline holding the old name in a secret store breaks, and the failure mode is a 401 with no diagnostic. The cost of keeping both is confusion, which this table pays off; the cost of unifying them is breakage with a deprecation window. Pinned by TestHowtoCredentialVocabularyIsPinnedPerFile and its siblings in internal/server/credential_vocabulary_test.go, plus TestSeedGuidesTeachOnlyTheCombinedTokenVocabulary in core/config/seed_guide_lint_test.go, so a guide cannot switch vocabularies silently.