Files
prompts/prompts/EXISTING_REPO_CHECKLIST.md
sneak 15b35ec3af
All checks were successful
check / check (push) Successful in 12s
Run every lint in a container via Dockerfile.lint (closes #40)
script/lint runs the linter directly when it is already inside a
container and otherwise builds Dockerfile.lint, so the linter never runs
on a developer host. That closes three host-only mechanisms: the result
cache golangci-lint keys on file content rather than location, which
produced a confirmed false green and findings reported against other
checkouts; the host-global $TMPDIR/golangci-lint.lock, which fails a run
in a way no caller can distinguish from findings; and host/container
version skew, which hid thirteen findings on one repo.

Detection is on LINT_IN_CONTAINER=1, set by every Dockerfile, and on
nothing else. The two directions are not symmetric: a false negative
inside a container attempts a nested docker build, finds no daemon and
fails loudly, while a false positive on a host silently lints there,
which is the defect this issue exists to kill. /.dockerenv is therefore
rejected even as a fallback -- measured absent inside BuildKit RUN steps
and present on any host that is itself a container, so it fails in both
directions and one of them is the dangerous one.

Nothing else changes shape. The Dockerfile still runs make check,
script/check still runs test, lint and fmt-check, script/cibuild is
still a single docker build with CHECK_EPOCH and VERSION, and the Go
multistage lint stage and its COPY --from=lint ordering dependency
survive with ENV LINT_IN_CONTAINER=1 added. Dockerfile.lint is the
standalone developer-host path and carries the same CHECK_EPOCH guard,
with the ARG below the dependency layer so only the lint re-runs.

The script/bootstrap golangci-lint install and the per-checkout
GOLANGCI_LINT_CACHE/TMPDIR wrapper are deleted as superseded. Neither
has a caller left. A JS repo's yarn install stays: the rule is that no
lint verdict may come from a host invocation, not that no linter binary
may exist there, and in a repo whose formatter is its linter the
formatter necessarily runs on the host.

golangci-lint config verify is kept, on measurement. Under the pinned
v2.12.2 a bogus top-level key and a bogus key under linters.settings.lll
both pass `golangci-lint run` with exit 0 and `0 issues` while config
verify exits 3 and names them; an unknown linter name fails run and
passes config verify. It needs no network: every case reproduced
byte-identically under `docker run --network none`, in a container where
`getent hosts golangci-lint.run` exits 2.

Comment blocks were cut hard across every file this unit touches.
.dockerignore drops from 67 comment lines to 28, script/cibuild from 17
to 12, script/docker from 18 to 12, and prompts/REPO_POLICIES.md from
1182 lines to 907. What remains says why a line is load-bearing; the
discovery narratives are gone.

config verify lives in script/lint's native branch rather than in a
Dockerfile, so every path that lints inherits it: the lint stage of the
main image, which is what CI runs, as well as Dockerfile.lint. Putting
it in one Dockerfile is how the other path silently loses it.

Model: opus-5
2026-09-08 04:10:17 +00:00

10 KiB

title, last_modified
title last_modified
Existing Repo Checklist 2026-08-10

Use this checklist when beginning work in a repo that may not yet conform to our repository policies (https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md).

Work on a feature branch. Check each item and fix any gaps before proceeding with your task.

Formatting (do this first)

  • If the repo has never been formatted to our standards, run make fmt and commit the result as a standalone branch/commit/PR before any other changes. Formatting diffs can be large and should not be mixed with functional changes.

Required Files

  • README.md exists with all required sections (Description, Getting Started, Rationale, Design, TODO, License, Author)
  • LICENSE file exists and matches the README
  • REPO_POLICIES.md exists and version date is current — fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/REPO_POLICIES.md
  • .gitignore is comprehensive (OS, editor, agent scratch, language artifacts, secrets) — fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore if missing. An existing repo usually has a hand-written one that is never re-fetched, so check the entries rather than the file's presence: .claude/ in particular, unanchored, so agent worktrees cannot be committed by accident. Do not give it a **/ prefix — that is a .dockerignore form and is wrong here.
  • .editorconfig exists — fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/.editorconfig
  • Dockerfile and .dockerignore exist (fetch .dockerignore from https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore); Dockerfile runs make check as a build step, and every stage containing a check-running RUN declares ARG CHECK_EPOCH with the RUN [ -n "$CHECK_EPOCH" ] || exit 1 guard immediately below it — see the CHECK_EPOCH rule in REPO_POLICIES.md. Without them the check layer is served from cache on an unchanged tree and the build reports a green it never ran.
  • Every stage that runs checks sets ENV LINT_IN_CONTAINER=1 — the lint stage and the build stage both. This is the item an existing repo most often fails after adopting the containerised lint: without it script/lint tries to build Dockerfile.lint from inside a build step, where there is no daemon.
  • Dockerfile.lint exists and script/lint builds it when not already in a container — see the containerised-lint rule in REPO_POLICIES.md. Base image pinned by sha256 with a version/date comment, ARG CHECK_EPOCH after the dependency layer with the guard below it.
  • .dockerignore excludes the repo's own host-built artifacts (compiled binaries, test binaries, coverage output), written root-anchored — /myapp, never **/myapp, which would also match cmd/myapp/. An existing repo is where such a binary is likeliest to already be sitting in the build context, invisible to git.
  • .dockerignore excludes .claude, root-anchored and with no **/ prefix. Agent worktrees are entire checkouts of the repo, so they inflate the context by a multiple of it and can copy another session's unreviewed work into an image layer. Confirm by enumerating the image, not by reading the file — .gitignore hides these from git status too.
  • Do agents in this repo run anywhere other than the repo root? The scratch directory is created in the agent's working directory, so the canonical anchored entry misses services/api/.claude/ in a monorepo with a per-service agent — it still reaches the build context and the image. An existing repo is where such a layout already exists, so check it here rather than assuming the canonical entry covers you: add anchored entries for the subdirectories that have one (/services/api/.claude), or **/.claude once you have confirmed no legitimately named nested directory would be caught.
  • If the repo embeds a version in a binary, that version is computed on the host and passed with --build-arg VERSION=... by script/docker and script/cibuild. No stage calls git describe: .dockerignore excludes .git, so it yields an empty version without failing the build. A tag-derived version additionally needs fetch-depth: 0 on the CI checkout step, which clones shallow and fetches no tags by default.
  • Every depth-independent pattern in .dockerignore carries a **/ prefix; only genuinely root-anchored entries such as .git are unprefixed, and .gitignore's patterns have not been transplanted unmodified. .dockerignore anchors an unprefixed pattern at the context root, so the transplanted form leaves config/.env and certs/server.key in the build context while reading as solved — see the .dockerignore rule in REPO_POLICIES.md.
  • Gitea Actions workflow in .gitea/workflows/ runs script/cibuild on push — reference https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitea/workflows/check.yml
  • Language-specific config:
    • Go: go.mod, go.sum, .golangci.yml (fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml)
    • JS: package.json, yarn.lock, .prettierrc, .prettierignore (fetch from https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierrc and https://git.eeqj.de/sneak/prompts/raw/branch/main/.prettierignore)
    • Python: pyproject.toml
    • Docs/writing: .prettierrc, .prettierignore (same URLs as above)

Makefile and script/ Entrypoints

  • Makefile exists in root — reference https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile
  • Has targets: test, lint, fmt, fmt-check, check, docker, hooks
  • Target implementations live in script/ (scripts-to-rule-them-all); Makefile targets are thin shims calling them — model scripts at https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>
  • script/precommit exists and the pre-commit hook (installed by script/install-precommit, shimmed by make hooks) runs it
  • README has an Entrypoints section documenting the script/ entrypoints and linking the standard
  • script/lint is the canonical detect-and-branch form, and no host invocation anywhere in the repo can produce a lint verdict — grep for the linter's own name across script/, the Makefile and CI config, not just script/lint. A second path is likeliest here: a make lint-fast, an older container-versus-host branch, or a CI step calling the binary directly. Expected hits that are not the defect: script/fmt, and in a repo whose formatter is also its linter, script/fmt-check. Everything else the grep finds is a real second path and goes.
  • Detection is on LINT_IN_CONTAINER alone. Reject any /.dockerenv or cgroup heuristic: absent in BuildKit RUN steps, present on hosts that are themselves containers, and a false positive lints on the host.
  • script/bootstrap installs no golangci-lint. Delete the block, its version and ref variables, and its call site. A JS repo's yarn install stays — it brings a linter along with every other dependency, which is fine as long as no verdict is taken from it.
  • The per-checkout lint state is gone: no GOLANGCI_LINT_CACHE or TMPDIR exports, no --allow-serial-runners, and .lint-cache/ removed from .gitignore and .dockerignore.
  • make check does not modify any files in the repo
  • make test has a 90-second timeout and completes within the 60-second hard cap (over 20 seconds is green but must be filed as an improvement bug)
  • make test runs real tests, not a no-op (at minimum, import/compile check)
  • make check passes on current branch

Formatting

  • Platform-standard formatter is configured (black, prettier, go fmt)
  • Default formatter config, only exception: four-space indents (except Go)
  • All files pass make fmt-check

Git Hygiene

  • Pre-commit hook is installed (make hooks)
  • No secrets in the repo (.env, keys, credentials)
  • No mutable references in Dockerfiles or scripts (tags, @latest) — all pinned by cryptographic hash with version/date comment
  • Using yarn, not npm (JS projects)

Directory Structure

  • No unnecessary files in repo root
  • Files organized into canonical subdirectories (bin/, cmd/, docs/, internal/, static/, etc.)
  • Go migrations in internal/db/migrations/ and embedded in binary

HTTP Service Hardening (if targeting 1.0 and the repo is an HTTP/web service)

  • Security headers set on all responses (HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy)
  • Request body size limits enforced on all endpoints
  • Read/write/idle timeouts configured on the HTTP server (slowloris defense)
  • Per-handler execution time limits in place
  • Password-based auth endpoints are rate-limited
  • CSRF tokens on all state-mutating HTML forms
  • Passwords hashed with bcrypt, scrypt, or argon2
  • Session cookies use HttpOnly, Secure, and SameSite attributes
  • True client IP correctly detected behind reverse proxy (trusted proxy allowlist configured)
  • CORS restricted to explicit origin allowlist for authenticated endpoints
  • Error responses do not leak stack traces, SQL queries, or internal paths

Final

  • make check passes
  • make lint runs twice on an unchanged tree with the lint layer DONE both times, never CACHED and never sub-second
  • script/cibuild succeeds (a bare docker build . or docker build -f Dockerfile.lint . fails closed by design, on the CHECK_EPOCH guard)
  • Commit and merge fixes before starting your actual task