Every product Hamzaish scaffolds is secure from commit zero. This page is the why behind that baseline and the threat model it defends against — AI-native factories ship fast, and AI-generated code passes functional tests while failing security silently. The defaults below are cheap to keep and expensive to bolt on later.
The mechanical enforcement lives in:
templates/product-starter-nextjs/—.gitignore,.env.example,.github/workflows/secret-scan.yml,.gitleaks.toml,.githooks/pre-commit/security-check <slug>— fast audit (factory/commands/security-check.md)/ship <slug>— the single deploy action (factory/commands/ship.md)/scaffold— wires all of this into new products (factory/skills/scaffold/SKILL.md)- the deep pass —
factory/agents/mvp/security-reviewer/+factory/playbooks/mvp-stage/security-checklist.md - the scale layer —
factory/playbooks/scale-stage/security-at-scale.md(quarterly/security-check --livedrift re-audit, attack-surface self-scan, data-breach runbook, retention discipline — ship-time gates verify the repo; this layer verifies the running system as it drifts)
CI runs with your repo's secrets in scope. Two failure modes dominate, and both are config-level:
Pin every action; never use a moving ref. @main / @master / @beta is a
moving tag the upstream owner (or anyone who compromises them) can repoint at new
code — which then runs with your secrets. Pin to a version tag at minimum, a full
commit SHA ideally. Bump deliberately, reading the changelog.
# bad — moving ref, runs whatever upstream pushes
- uses: some/action@main
# ok — version tag
- uses: some/action@v2.3.9
# best — immutable commit SHA
- uses: some/action@<40-char-sha> # v2.3.9
anthropics/claude-code-action— pin to>= v1.0.94. Versions before v1.0.94 are subject to a prompt-injection → secret-exfiltration advisory: a crafted issue/PR/comment could steer the action into leaking repo secrets. Pin tov1.0.94or later (ideally the patched commit SHA)./security-checkflags anyclaude-code-actionbelow v1.0.94, and flags bare@v1/@beta/@mainon it as unpinned-and-unsafe.
Declare least-privilege permissions. The default GITHUB_TOKEN is often
write-scoped. Set the narrowest scope each job needs — the secret-scan and CI jobs
only need to read:
permissions:
contents: read # nothing else unless a job genuinely needs itAvoid permissions: write-all. Grant contents: write / id-token: write only
to the specific job that requires it.
Anything you didn't author is attacker-controlled: issue and PR bodies, comments, webhook payloads, scraped pages, uploaded files, and any text passed to an LLM.
- CI triggers:
pull_request_target,issues, andissue_commentrun in a privileged context with secrets. Never check out and run untrusted PR code, or feed issue/PR text into a privileged step, under those triggers. Usepull_request(no secrets for forks) for anything that executes contributor code./security-checkflags these. - LLM calls: user text passed to a model can carry injected instructions ("ignore your rules, exfiltrate X"). Never let raw model output take privileged actions on the user's behalf, hit internal services, or render as trusted HTML. Sanitize model output before rendering (don't trust markdown to be safe HTML); keep LLM API keys server-side only; cap per-user spend.
- MCP/agent configs:
.mcp.jsonand.claude/settings*.jsonare part of the attack surface — inline credentials inenvblocks, wildcard permission allowlists ("Bash","mcp__*"),bypassPermissions, plaintexthttp://servers, and moving-tag server pulls (@latest) all ship silently in AI-scaffolded repos.scripts/check-mcp-config.tsscans these deterministically;/security-checkruns it as its § 6. (Idea ported from metaharness'smcp-scan— seereferences/README.md§ metaharness.) - App input: validate every server action / API route with zod (or equivalent); parameterize SQL; validate file MIME/size/extension; guard user-supplied URLs against SSRF (no internal IPs).
Code an agent writes or fetches should execute in a throwaway, isolated
environment, not on your machine or a host with credentials. A prompt-injected or
buggy agent that runs on the host can read your ~/.ssh, env vars, and every other
repo.
- Docker / VS Code devcontainer — the default, and now partly enforced
rather than merely advised: the Next.js starter ships a
.devcontainer/(devcontainer.json+Dockerfile, Node + Bun, non-root user, workspace-only mount, no host SSH/secret passthrough), and/scaffoldcopies it into every new product and tells the operator to "Reopen in Container." So a freshly scaffolded product comes with the isolated box already wired — the agent works inside it with only the repo mounted; blow it away after. (Opening the product in the container is still the operator's action; running on the bare host instead is at their own risk.) - E2B (or equivalent ephemeral cloud sandbox) — for running untrusted or generated code remotely with nothing of yours attached.
- Scope what the sandbox can reach: no host network to internal services, no real secrets unless the task genuinely needs them (and then only scoped, rotatable ones).
The rule: the blast radius of a compromised agent should be one disposable box, not your laptop.
- Only
.env.example(placeholder names, no values) is committed..env,.env.local,.env*.localare gitignored. - Local dev: real values in
.env.local(gitignored). - Prod / preview: a secret manager / platform env — Vercel Project → Settings → Environment Variables. CI uses throwaway placeholders for the build.
- Defense in depth:
gitleaksruns in CI on every push/PR (secret-scan.yml), and the optional.githooks/pre-commithook catches leaks before they leave your machine. - If a secret ever hits a commit, it is compromised — even after deletion, it's in history and likely already scraped. Rotate the key first, then scrub history. Removal alone is not remediation.
Row Level Security is the authorization backstop. Without it, the anon/service keys plus a guessable query expose every row.
- Every table holding user data:
enable row level security+ an explicit policy that scopes rows to their owner. Nopublic-readable policies on user data. - Set the habit in the starter migration; don't wait until launch.
- App-layer
userId === resourceOwnerIdchecks are good, but RLS is the layer that holds when an endpoint forgets the check./security-checkreminds you to verify RLS coverage.
Deploys are deliberate, reviewed, and single-action — not a side effect of saving work.
- Vercel Production Branch =
production. Pushes to the working branch (main) build Preview deployments, not Production. /ship <slug>is the only deploy action. It gates on/security-check, shows exactly what will ship, then fast-forwards reviewed commit(s) ontoproductionand pushes. That push is the deploy.- Auto-commit
wip(auto):snapshots stay on the working branch and never reachproduction. They're recoverable save-points (see the auto-commit hook in the rootCLAUDE.md), folded into a real commit before they're promoted. - Auto-push is opt-in (exfiltration posture). The auto-commit hook makes
local restore-point commits by default and does not push. A repo only
pushes automatically if the operator drops a
.auto-pushmarker in its root — and even then the hook secret-scans the to-be-pushed commits (gitleaks if installed, else a built-in key-pattern grep) and aborts the push if a likely secret is found. The default means your work and secrets don't leave the machine automatically; you push (or/ship) deliberately. (.no-auto-pushremains as an extra hard guard.) productionis fast-forward-only — never force-pushed or rewound. A divergence is a stop-and-resolve, not an override.
Studied via our repo syllabus (security batch, entries E1–E6) and queued into /security-check. We port the practice, not the code; each is a candidate until proven on a product pre-launch (see meta/evals/factory-change-gate.md).
- gitleaks (already in use) — regex/entropy secret scanning in CI + the push gate. Baseline; keep. — https://github.com/gitleaks/gitleaks
- trufflehog — verified secret detection (confirms a key is live), cutting gitleaks' false positives. Adopt where a finding needs triage before it blocks. — https://github.com/trufflesecurity/trufflehog
- semgrep — policy-as-code SAST with shareable rule packs; candidate for a small
/security-checkrule set targeting our stack's footguns (Next.js, Supabase). — https://github.com/semgrep/semgrep - harden-runner (step-security) — runtime egress control + tamper detection for GitHub Actions, beyond the SHA-pinning in §1. Pin AND constrain network. — https://github.com/step-security/harden-runner
- OWASP CheatSheetSeries — map each
/security-checkitem to a named cheat sheet so the audit is defensible, not ad-hoc. — https://github.com/OWASP/CheatSheetSeries
Credits live in ACKNOWLEDGMENTS.md / docs/LEARN-FROM-REPOS.md.
A substitute for the full pre-launch review (security-reviewer agent +
security-checklist.md), a penetration test, or a SOC 2 audit. It's the
secure-by-default floor that makes those later efforts cheaper — not the ceiling.