Our own audit. We ran /production-audit on Foundry's own public repo. Check it against the source.
All reports →Audit report · v0.2.0
Foundry
Release recommendation
Safe to ship
Static review · runtime not exercised
Healthy. Ten dimensions and six adversarial verifications found one risk, a vulnerable framework pin, and it was fixed in this branch before publication; what remains is a robustness punch list on a small, deliberately fail-closed surface.
Why this verdict
Static review, runtime not exercised. Ten dimension reviews produced six risk candidates; adversarial verification confirmed one (a Next.js version in range for an unauthenticated Server Actions CPU-exhaustion advisory), which was fixed in the audited branch (next 16.3.1) and re-verified before this report was emitted. The five others were downgraded to improvements on the reachable-today rule, each with its reasoning recorded. The access-control, origin-pinning, and fail-closed auth surfaces verified clean at every layer checked. The runtime checks a static run cannot perform are listed under notAssessed.
Not assessed this run: data-migration-safety
What's solid (10)
Authorization
The authz matrix closed with no gaps: every gated read enforces owner-or-admin in the data-access layer itself (lib/reports.ts canView/getReportFor/getPublicReport), generateMetadata goes through the same gate, non-owner and nonexistent slugs are indistinguishable 404s, and no state-mutating endpoint exists beyond auth itself.
Path traversal
All three dynamic slug routes are traversal-proof in the data layer (strict slug regex plus directory-prefix check in lib/reports.ts:92-94 and lib/skills.ts:193-195), and the Behind-the-Build fs reads are hard-whitelisted (lib/docs.ts ALLOWED set); NOTES/PROMPTS/FRICTION are unreachable through any route.
Auth flow integrity
No open redirect exists (fixed-path redirects built from a server-pinned origin; SITE_ORIGIN set in fly.toml so the emailed magic-link target is never derived from a spoofable forwarded host); session trust is JWT-validated via getUser() on every access decision; a failed confirm buffers no cookies and can never half-establish or clobber a session (lib/supabase.ts applyCookies).
Fail-closed posture
Missing auth env locks the gated surface closed with a one-time ops log at every consumer; a malformed report file is logged and skipped rather than 500ing the index; the VERSION file read fails open to 0.0.0, the one direction that can never falsely tell an installed skill it is behind.
Analytics isolation
Client and server capture no-op without their keys, are never awaited in a request path, and never gate a response; the version endpoint validates against the canonical skill allowlist, disables person-profile processing on its synthetic actor, and serves Cache-Control: no-store.
Accessibility primitives
Skip link, global focus-visible ring, blanket and per-effect reduced-motion kills, aria-live slide announcements, labeled form fields with described errors and aria-busy pending state, 44px targets throughout, and severity, verdict, and gate status always carried by text, never color alone.
Design-token fidelity
globals.css matches DESIGN.md hex for hex including the AA-driven brightened faint tone; no raw hex outside documented mirrors; the display face ships in its single loaded weight; the report-excerpt color exception is honored at all four codified sites; glossary and no-em-dash rules hold across every surface swept including all eleven SKILL_COPY entries.
Runtime image completeness
Every runtime fs-read target is explicitly COPYd into the standalone image (VERSION included), cross-checked against every readFileSync/readdirSync call site; build-arg keys never become runner env; security headers enforce on every route; no dev or debug endpoint exists.
Version-marker integrity
The VERSION file is a single source of truth mechanically enforced: make validate fails on drift in any SKILL.md frontmatter or check URL, SECURITY.md, or a SKILL_SLUGS/skills mismatch in either direction, and CI runs it on every PR and push. The in-skill check command enforces its own opt-out, rate limit, timeout, and fail-silence.
Verified fix loop
The one confirmed risk of this audit (next 16.2.9 in range for GHSA-m99w-x7hq-7vfj) was fixed in the audited branch and re-verified (next 16.3.1 resolved in the lockfile, build and typecheck green, advisory absent from the post-fix npm audit) before this report was published, alongside two skeptic-verified truth defects: the homepage verdict hue now derives from the verdict via a full-literal class map, and the CI comment now claims detection, not an enforcement no ruleset provides.
Risks to weigh (0)
Findings with a path to harm in production. These are what the verdict weighs.
No risks surfaced. Nothing here has a path to harm in production.
Improvements (39)
Safe today. Ways to make the app more robust, observable, and consistent over time. These do not affect the verdict.
SEC-01mediumPrivate-report confidentiality rests entirely on the site gate while report storage is a public git repository.security · lib/reports.ts:5
REL-01mediumMagic-link confirmation consumes the single-use token on a bare GET, so corporate mail scanners that prefetch links can spend it before the user clicks.reliability · app/auth/confirm/route.ts:24
Mechanism confirmed real and vendor-documented, but the affected population does not exist today (both committed reports are public with no owner; ADMIN_EMAILS is a gmail mailbox), so the risk classification fails the reachable-today rule. Report as a medium improvement with a pre-onboarding mitigation.
OPS-01mediumNo alerting path with a named receiver exists anywhere: the health check only proves the static homepage renders, logs are ephemeral stdout on auto-stop machines, and nothing distinguishes analytics-down from traffic-zero.operability · fly.toml:36
OPS-02mediumThe magic-link request failure path is the one critical-flow failure with neither a log nor an event.operability · app/unlock/actions.ts:40
TEST-01mediumZero application tests exist and no runner is installed; the deterministic gates comprehensively pin the report contract and version markers while pinning nothing about the running site (one root cause; the unpinned controls are instanced).testing-confidence · package.json:8
Argued up to risk by the finder; the skeptic rejected the classification: the control works today, zero private reports exist to expose, one of the two swaps fails typecheck in CI, and the harm requires compound future events. Kept medium as the improvement's severity for the day the preconditions change.
SHIP-01mediummain is the live npx install channel and carries no branch ruleset; CI detects but nothing blocks a direct push.release-safety · .github/workflows/ci.yml:10
Proposed medium risk; downgraded on reachability (single-writer org verified live, three-failure chain required, detection exists) and on the suite's own SHIP-01 precedent, which classified the strictly wider gap as an improvement. The CI comment that claimed enforced merging was a separate docs-truth defect, fixed in the audited branch.
SHIP-02mediumThe validated artifact is never the shipped artifact: deploy rebuilds from the local working tree, and the image carries no revision stamp.release-safety · Dockerfile:16
SHIP-03mediumNo named post-deploy verification exercises a user-visible outcome on either channel; the health check proves the homepage returns 200.release-safety · fly.toml:36
SHIP-04mediumRollback is undocumented on both channels, and a skill recall cannot reach existing installs without a forward version bump plus a site deploy.release-safety · TODOS.md:117
PERF-01mediumThe open version endpoint amplifies each valid unauthenticated hit into an immediate outbound PostHog TLS POST with no rate limit or backpressure bound.performance-capacity · app/api/version/route.ts:40
29 more · hygiene and polish›
SEC-02lowThe unauthenticated email-send action has no application-layer throttle; the effective limits are Supabase's.security · app/unlock/actions.ts:35
Filed as a medium risk without checking the platform layer the kill list requires. Supabase enforces server-side OTP rate limits and the project's recorded email cap is 2/hour, collapsing both claimed harms to nuisance level; the rubric names this exact case (a low rate-limit gap mitigated upstream) as an improvement.
SEC-03lowThe pre-auth analytics event is keyed on the raw email a visitor typed, which may be a third party's.security · app/unlock/actions.ts:46
REL-02lowEvery confirmation failure, including a Supabase outage, is reported to the user as an expired link.reliability · app/auth/confirm/route.ts:33
REL-03lowThe GitHub star fetch has no timeout.reliability · components/InstallBlock.tsx:26
REL-04lowNo Supabase call carries an explicit timeout, and the failure envelope leans on platform defaults (one root cause; instances listed).reliability · lib/supabase.ts:129
CON-01lowA second magic-link request likely invalidates the first link's token, and the rate-limit retry copy understates the real window.concurrency · app/unlock/actions.ts:35
CON-02lowAn already-signed-in visitor to /unlock (the loser tab of a two-tab confirm, or a stale-link click) is shown the sign-in form and an expired error instead of their session.concurrency · app/unlock/page.tsx:1
A11Y-01lowFocus is dropped when the deck's Prev/Next button disables under the keyboard user at either extreme.accessibility · components/SlideDeck.tsx:186
A11Y-02lowScorecard jump controls scroll without moving focus, and zero-count cells remain focusable no-op buttons (one root cause: focus not managed on the scorecard's interactive grid).accessibility · components/ReportScorecard.tsx:35
A11Y-03lowThe hairline border tokens sit below the 3:1 non-text contrast minimum for component boundaries.accessibility · app/globals.css:16
A11Y-04lowThe scorecard's hovered cell drops its small labels below AA contrast (4.19:1 computed).accessibility · components/ReportScorecard.tsx:74
UI-01lowThe meta descriptions still enumerate five promises; the site says six.ui · app/layout.tsx:9
UI-02lowThe install slug is hardcoded on two surfaces past the single source of truth that exists for it.ui · app/skills/page.tsx:75
UI-03lowThe unlock submit is the one primary CTA rendered all-caps against the brand's sentence-case button rule.ui · components/SubmitButton.tsx:22
UI-04lowA severity hue is used on non-report chrome (the unlock form error) where DESIGN.md licenses severity color only inside report excerpts.ui · app/unlock/page.tsx:93
INF-01lowThe runtime container runs as root; no USER directive exists.infra · Dockerfile:48
INF-02lowThe dockerignore excludes .env.local and .env.*.local but not a bare .env.infra · .dockerignore:9
OPS-03lowFailure diagnostics conflate causes: every verify error is captured as reason expired, and the version check's zero-events state is undocumented (broken endpoint and no adopters read identically).operability · app/auth/confirm/route.ts:58
OPS-04lowThe ops runbook is scattered (fly.toml comments, CLAUDE.md gotchas, an error boundary whose "was logged" claim is only true of the server digest), and the consolidation is already indexed but unshipped.operability · TODOS.md:117
TEST-02lownpm run validate:reports runs only the schema half of the gate, inviting a contributor to believe they ran make validate.testing-confidence · package.json:12
SHIP-05lowThe release protocol is designed but unexecuted: zero git tags exist, the procedure is not yet in CLAUDE.md, the changelog-touch CI check is absent, and the deployed endpoint can lag main for weeks, starving the update-prompt loop (grouped; all one protocol gap the repo already indexes as workstream A).release-safety · CHANGELOG.md:5
PERF-02lowSession lookups are un-memoized per render: a gated report view makes up to four Supabase getUser round trips (middleware, layout, metadata, page).performance-capacity · lib/supabase.ts:119
PERF-03lowThe reports index re-reads and re-parses every report file roughly three times per request, forever dynamic, with report count growing one file per audit.performance-capacity · lib/reports.ts:107
PERF-04lowThe version check's 10s timeout holds roughly a 2s margin over the measured cold start, and a timed-out check self-suppresses for the day.performance-capacity · fly.toml:31
SEC-04informationalThe CSP allows 'unsafe-inline' scripts, removing its value as an XSS backstop.security · next.config.ts:24
SEC-05informationalThe ?sent= query value is reflected (React-escaped) into a trust-bearing confirmation sentence.security · app/unlock/page.tsx:36
A11Y-05informationalScreen-reader announcement polish across four small surfaces (grouped; each is an announcement or semantics nit).accessibility · components/CopyButton.tsx:40
UI-05informationalDeck chips render command tokens in body gray, the only invocation strings on the site outside the amber command color.ui · components/SlideDeck.tsx:159
INF-03informationalThe image ships three docs nothing can read (NOTES, PROMPTS, FRICTION are outside the render whitelist), and the COPY comment plus the CLAUDE.md gotcha overstate the runtime-read set; the retained dev-only vulnerable ajv-cli chain also lives here as an accepted, documented exception.infra · Dockerfile:42
Prioritised remediation plan
- 01
Close the report-confidentiality seam before any private report lands
The publish-time validate control, the getReport unexport, and the first pinning tests are one small change set that converts the audit's two everything-depends-on-it improvements into enforced invariants.
SEC-01, TEST-01
- 02
Ship the Operations section and one external signal
The runbook TODOS already indexes, an uptime check on /api/version, the request-stage failure log, and the two documented recall paths.
OPS-01, OPS-02, OPS-03, OPS-04, SHIP-04
- 03
Execute the release protocol at the v0.1.1 roll-up
The main ruleset, the deploy guard with a revision stamp, the named post-deploy checks, tags, and the changelog CI gate are all already-designed workstream A items.
SHIP-01, SHIP-02, SHIP-03, SHIP-05
- 04
Pre-onboarding auth hardening
The interstitial confirm page, honest failure copy, the signed-in /unlock redirect, and the pre-auth identity hygiene, all before the first external report owner.
REL-01, REL-02, CON-01, CON-02, SEC-02, SEC-03
- 05
Accessibility and polish pass
Focus management on the deck and scorecard first; the rest is one short sweep.
A11Y-01, A11Y-02, A11Y-03, A11Y-04, A11Y-05, UI-01, UI-02, UI-03, UI-04, UI-05
Quick wins: low regression risk
Add .env to .dockerignore
Blast radius: One config line; no behavior change.
Wrap getSessionUser in React.cache
Blast radius: One import and one wrapper; removes up to three network calls per gated render.
Sixth promise into the two meta descriptions
Blast radius: Two strings; search previews only.
Interpolate REPO_SLUG on the two hardcoded install commands
Blast radius: Two template literals.
AbortSignal.timeout on the GitHub star fetch
Blast radius: One fetch option; build-time resilience only.
Requires deeper investigation
Supabase dashboard configuration
The email rate limits, template link shape, and redirect allowlist all live in dashboard state the repo cannot show; SEC-02 and REL-01's mitigations and the cutover checklist depend on them.
Version-check endpoint under load
PERF-01's degradation shape (connection growth vs posthog-node's internal queue absorbing the burst) is only sizeable with a live load test against the Fly machine.
Not assessed: runtime-only checks skipped
Device-level mobile verification matrix
To verify: Run the mobile skill's matrix (widths, landscape, text zoom, keyboard, real devices) against the current surface.
Identity stitch end to end
To verify: One magic-link sign-in with capture enabled; confirm PostHog shows one actor across the pre-auth and authed events.
Mail-scanner magic-link consumption
To verify: Send a link to a Defender SafeLinks or Mimecast test mailbox; observe whether the human click lands expired.
Rendered contrast and screen-reader behavior
To verify: axe plus VoiceOver/NVDA pass on the deck, scorecard, copy confirmation, and unlock error announcement.
Cold-start latency and the check's 10s margin
To verify: Stop the machine, time five cold curls of /api/version, compare p95 to the timeout.
Secret scan with a real scanner over tracked history
To verify: gitleaks or trufflehog across the repo history (the public history is a single orphan commit, so the surface is small).
Deployed-artifact identity
To verify: After the next deploy, confirm the live /api/version equals the repo VERSION and the image was built from a clean main checkout.
Mechanical sweeps
- dependency-auditfindingsPre-fix: 9 high, 1 moderate. The one reachable advisory (next 16.2.9, unauthenticated Server Actions CPU exhaustion, GHSA-m99w-x7hq-7vfj) was confirmed by an adversarial skeptic and fixed in-branch (next 16.3.1, non-major), which also cleared the sharp, postcss, and server-function-disclosure advisories; npm audit fix swept the dev-only brace-expansion pair. Post-fix: 2 high remain, both in the ajv-cli devDependency chain (validation tooling, never shipped); npm's proposed fix is a breaking downgrade and was rejected on the record (see INF-03).
- secret-scancleanFallback pattern grep over source and config found no secret material; the only committed key is the PostHog browser token, public by design. A real scanner over tracked history remains worth one run; see notAssessed.
- typecheckclean
- lintclean