The canonical .dockerignore and .gitignore both omitted the in-repo agent scratch directory. On this fleet that directory holds one worktree per in-flight agent -- an entire additional checkout of the repo each -- so under `COPY . .` all of it reached the build context and the image. Measured on this repo before the change: five planted scratch files, at every depth beneath the directory, all present inside a probe image built from the real context. Three consequences, only the first of which is about size. The context inflates by a multiple of the repo. Another session's unreviewed and sometimes uncommitted work is copied into a build artifact. And the directory is created and destroyed constantly by tooling, so it invalidates `COPY . .` for reasons that have nothing to do with this repo's content -- which is the accidental cache protection described at length in the issue thread, and the reason this change was sequenced behind the CHECK_EPOCH bust rather than landed alongside the rest of the .dockerignore work. The two entries are deliberately different shapes, because the two files have different semantics and neither is derived from the other. In .dockerignore the entry is anchored, `.claude`, with no `**/` prefix: the directory occurs exactly once, at the context root, and the prefixed form additionally matches any nested directory of that name. Measured rather than argued -- the `**/`-prefixed control was built and enumerated too, and it removes prompts/.claude/ from the context as well, which in a repo with a legitimately named nested directory would silently delete it from the build. In .gitignore the entry is unanchored, `.claude/`, because a .gitignore pattern already matches at every depth; `git check-ignore -v` confirms it covering both .claude/ and prompts/.claude/, so a `**/` prefix there would be redundant at best, and on an anchored pattern it would be actively wrong. Anchoring buys that at the cost of a residual exposure, and the vendored files now say so rather than only asserting the reason to anchor. "Occurs exactly once, at the context root" is a property of how agents are run, not of the tooling: the directory is created in the agent's working directory, so a monorepo running a per-service agent in services/api/ still ships services/api/.claude/ into the context and the image -- the exact exposure this change exists to close, left open in the repo shape where it is likeliest. The .dockerignore header block, the REPO_POLICIES.md bullet and both checklists state the gap and the remedy (anchored entries for the subdirectories that have one, or `**/.claude` once no legitimately named nested directory would be caught). Consuming repos receive the files and not the tracker, so a caveat that lives only in a PR body is not a caveat. It is not case-folded the way the neighbouring secret patterns are: tooling creates the directory in exactly one spelling, so a folded pattern would add no coverage. The earlier justification -- that a miss costs bloat rather than exposure -- is gone, because it contradicted this issue's own framing, in which the cost of a miss is unreviewed work in an image layer. The second half of this change is the consequence that ships broken silently. Excluding .git means `git describe` cannot run in any build stage, and it fails quietly there rather than erroring: `-X main.Version=` comes out empty, the binary reports no version, and the build still exits 0. The Go template in REPO_POLICIES.md had `ARG VERSION=dev` and never said where VERSION came from, which is precisely the gap a reader fills in with `git describe` inside the build. It now says: computed on the host, threaded in with `--build-arg VERSION=...`, shown as a complete command rather than as two rules each documenting half of one. script/docker and script/cibuild do it, with the same discipline the epoch already has -- assignment on its own line, because a failing command substitution inside an argument does not trip `set -e`, plus a non-empty fallback so a build from an export with no .git reports `unknown` rather than an empty string that reads as a successful version. That fallback is applied in exactly one place, and that place can actually execute. `git describe ... || true` leaves the value empty when it fails, and the `[ -n "$version" ]` line is what substitutes `unknown`. Folding the fallback into the substitution as `|| echo unknown` would have left the guard unreachable -- harmless in itself, but a guard that cannot fire is indistinguishable from one that works to every repo that copies it, and canonical text should not carry a check that is decorative. Measured firing in both sh and dash: not a git repository -> unknown, repository with no commits yet -> unknown, this repository -> the describe output. The checklist now carries the guard line as well, so the vendored guidance and the vendored script no longer disagree. The scripts pass VERSION unconditionally rather than growing a per-repo variant. This repo's Dockerfile declares no `ARG VERSION`, and BuildKit was measured accepting the unconsumed arg silently -- no warning, no cache effect, confirmed by the paired runs below in which the bootstrap layer still caches. The alternative, leaving it to each repo, reintroduces the trap: a repo that needs a version and finds no VERSION in its scripts writes `git describe` into the Dockerfile, which is the failure being closed. The same correction reaches the two Go documents that carry the GOLDFLAGS pattern, since a `$(shell git describe)` evaluated inside a build stage is exactly this empty version. Both are now `?=`, and their comments say precisely when that matters: where a build stage compiles by invoking make, `ARG VERSION` puts the value in the environment and `?=` defers to it, whereas the canonical Go template compiles with `go build` directly and uses the Makefile on the host only -- still `?=`, so that a repo which later moves its build behind make does not silently start shipping an empty version. Leaving them as `:=` would have left the corpus telling a reader one thing in the policy and the opposite in the styleguide. Both repo checklists gain the entries too. They are what an agent reads while writing these files, so they are where the wrong shape actually gets written: the .gitignore item is the one an existing repo never re-fetches, and it now names `.claude/` explicitly along with the warning not to prefix it. Verification, by enumerating a probe image rather than by reading the patterns. Standalone minimal Dockerfile held outside the context, `--no-cache` scoped to that one image, no prune of any kind. Before: all five planted scratch files in the image, 42 files total. After: zero, 37 files total, with README.md, script/check, prompts/NEW_REPO_CHECKLIST.md and a planted probe_src/app.md all still present as positive controls, so the exclusion is a real exclusion and not a COPY that stopped copying. Transferred context fell from 161.86kB to 68.82kB, recorded as corroboration only: BuildKit reports a delta, not a total, and an earlier run in this repo transferred 2.18kB while shipping 43 files. The CHECK_EPOCH verification was re-run under the changed context, because the context moved underneath the earlier measurement. Two consecutive script/cibuild runs on an unchanged tree: run 1 in 17.07s, run 2 in 5.73s, both executing the check layer with a distinct epoch and real prettier output from both lint and fmt-check. The `RUN script/bootstrap` layer is CACHED in run 2, which is the validity control -- it proves no concurrent prune landed between the runs and that no --no-cache path was taken, so the check layer executing is the bust working rather than a cold cache. Planted files were removed afterwards and their absence confirmed against the filesystem with `find`, not against `git status`, which cannot see them once .gitignore covers the directory -- the same blind spot that made the earlier secret exposure invisible. Model: opus-5
852 lines
48 KiB
Markdown
852 lines
48 KiB
Markdown
---
|
|
title: Repository Policies
|
|
last_modified: 2026-08-19
|
|
---
|
|
|
|
This document covers repository structure, tooling, and workflow standards. Code
|
|
style conventions are in separate documents:
|
|
|
|
- [Code Styleguide](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE.md)
|
|
(general, bash, Docker)
|
|
- [Go](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_GO.md)
|
|
- [JavaScript](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_JS.md)
|
|
- [Python](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_PYTHON.md)
|
|
- [Go HTTP Server Conventions](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/GO_HTTP_SERVER_CONVENTIONS.md)
|
|
|
|
---
|
|
|
|
- Cross-project documentation (such as this file) must include
|
|
`last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync
|
|
with the authoritative source as policies evolve.
|
|
|
|
- **ALL external references must be pinned by cryptographic hash.** This
|
|
includes Docker base images, Go modules, npm packages, GitHub Actions, and
|
|
anything else fetched from a remote source. Version tags (`@v4`, `@latest`,
|
|
`:3.21`, etc.) are server-mutable and therefore remote code execution
|
|
vulnerabilities. The ONLY acceptable way to reference an external dependency
|
|
is by its content hash (Docker `@sha256:...`, Go module hash in `go.sum`, npm
|
|
integrity hash in lockfile, GitHub Actions `@<commit-sha>`). No exceptions.
|
|
This also means never `curl | bash` to install tools like pyenv, nvm, rustup,
|
|
etc. Instead, download a specific release archive from GitHub, verify its hash
|
|
(hardcoded in the Dockerfile or script), and only then install. Unverified
|
|
install scripts are arbitrary remote code execution. This is the single most
|
|
important rule in this document. Double-check every external reference in
|
|
every file before committing. There are zero exceptions to this rule.
|
|
|
|
- Every repo with software must have a root `Makefile` with these targets:
|
|
`make bootstrap`, `make setup`, `make test`, `make lint`, `make fmt` (writes),
|
|
`make fmt-check` (read-only), `make check` (runs `test`, `lint`, `fmt-check`),
|
|
`make docker`, and `make hooks` (installs pre-commit hook). A model Makefile
|
|
is at `https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`.
|
|
|
|
- Repos follow the
|
|
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
|
pattern: the implementation of each Makefile target lives in an executable
|
|
script in `script/` (`script/bootstrap`, `script/setup`, `script/test`,
|
|
`script/lint`, `script/fmt`, `script/fmt-check`, `script/check`,
|
|
`script/docker`), and the Makefile targets are thin shims that call them. The
|
|
scripts must be POSIX sh (`#!/bin/sh`, `set -eu`, no bashisms) so they run in
|
|
minimal containers (e.g. alpine images have no bash); locate the repo root
|
|
with `$(cd "$(dirname "$0")/.." && pwd -P)` and `cd` there before acting. From
|
|
the standard's canonical set we use `bootstrap`, `setup` (make the repo ready
|
|
for development after a fresh clone: runs `bootstrap`, then
|
|
`install-precommit`, plus any repo-specific initialization), `test`, and
|
|
`cibuild`. `script/bootstrap` installs all dependencies idempotently and
|
|
assumes nothing is present: base tools come from nix, apt, brew, or apk
|
|
(detected in that order; apt runs noninteractive). For node it uses the
|
|
installed node if present; otherwise it installs a PINNED node version via
|
|
nvm, first installing nvm itself if missing — from a hash-verified GitHub
|
|
release archive (never `curl | sh`), with bash installed as an explicit
|
|
prerequisite since nvm requires bash. yarn is then pinned via
|
|
`corepack prepare yarn@<version> --activate`. Never install "latest" or "lts";
|
|
always exact versions. `script/cibuild` runs the CI build: it changes to the
|
|
repo root and runs
|
|
`docker build --build-arg CHECK_EPOCH="$epoch" --build-arg VERSION="$version" .`,
|
|
where `epoch` is a per-invocation nonce (see the `CHECK_EPOCH` rule below) and
|
|
`version` is computed on the host because `.git` is not in the build context
|
|
(see the git-describe rule below); the Gitea workflow calls it. Four further
|
|
scripts are our own extensions to the standard: `script/check` runs
|
|
`script/test`, `script/lint`, and `script/fmt-check`; `script/precommit` is
|
|
what the git pre-commit hook runs, and it calls `script/check`;
|
|
`script/install-precommit` installs the git pre-commit hook (the `make hooks`
|
|
target shims to it); and `script/projectname` (literally that filename) simply
|
|
outputs the project's name. Scripts that need the name call
|
|
`script/projectname` — e.g. `script/docker` assembles its image tag from it —
|
|
so those scripts stay byte-identical across all repos. Repo-type-specific
|
|
pre-commit extras (e.g. `go mod tidy` verification in Go repos) belong in
|
|
`script/precommit`, not in the hook itself. Model scripts are at
|
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/script/<name>`. The README
|
|
must document the provided scripts in an **Entrypoints** section (see the
|
|
README requirements below).
|
|
|
|
- Always use Makefile targets (`make fmt`, `make test`, `make lint`, etc.)
|
|
instead of invoking the underlying tools directly. The Makefile is the single
|
|
source of truth for how these operations are run.
|
|
|
|
- The Makefile is authoritative documentation for how the repo is used. Beyond
|
|
the required targets above, it should have targets for every common operation:
|
|
running a local development server (`make run`, `make dev`), re-initializing
|
|
or migrating the database (`make db-reset`, `make migrate`), building
|
|
artifacts (`make build`), generating code, seeding data, or anything else a
|
|
developer would do regularly. If someone checks out the repo and types
|
|
`make<tab>`, they should see every meaningful operation available. A new
|
|
contributor should be able to understand the entire development workflow by
|
|
reading the Makefile.
|
|
|
|
- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check`
|
|
as a build step so the build fails if the branch is not green — which requires
|
|
`ARG CHECK_EPOCH` and its guard in every stage containing a check-running
|
|
`RUN`, per the `CHECK_EPOCH` rule below. Without them a Dockerfile satisfies
|
|
this criterion while its check layers are served from cache, so the build
|
|
cannot fail on a branch that is not green. For non-server repos, the
|
|
Dockerfile should bring up a development environment and run `make check`. For
|
|
server repos, `make check` should run as an early build stage before the final
|
|
image is assembled. Dockerfiles install development prerequisites by running
|
|
`script/bootstrap` rather than duplicating installs inline; COPY `script/` and
|
|
the dependency manifests (`package.json` + `yarn.lock`, `go.mod` + `go.sum`,
|
|
etc.) before running it so the bootstrap layer stays cached until dependencies
|
|
change.
|
|
|
|
- **Every check-running `RUN` must be cache-busted with `CHECK_EPOCH`.** Docker
|
|
invalidates a `COPY` layer only when the copied content changes, so on an
|
|
unchanged tree the `RUN make check` layer is served from cache, the suite
|
|
never runs, and the build still exits 0. A sub-second `docker build` reporting
|
|
success is a cache hit, not a result. The canonical form, in **every** stage
|
|
containing a check-running `RUN`:
|
|
|
|
```dockerfile
|
|
ARG CHECK_EPOCH
|
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make check
|
|
```
|
|
|
|
and in both `script/cibuild` and `script/docker`:
|
|
|
|
```sh
|
|
epoch="$(date +%s%N)$$"
|
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
|
[ -n "$version" ] || version="unknown"
|
|
docker build \
|
|
--build-arg CHECK_EPOCH="$epoch" \
|
|
--build-arg VERSION="$version" \
|
|
.
|
|
```
|
|
|
|
The `VERSION` lines are there for a different reason, covered by the
|
|
git-describe rule below; they are shown here so the two rules do not each
|
|
document half a command. All four `CHECK_EPOCH` elements are load-bearing;
|
|
none is optional, and each guards a failure mode that otherwise fails green:
|
|
- `ARG` is stage-scoped, so a single declaration leaves the other check
|
|
stages frozen while the fix reviews as complete. Declare it in every stage
|
|
that runs checks, immediately above the first such `RUN`.
|
|
- Expand the value into the command. This makes the cache miss contractual
|
|
rather than dependent on BuildKit's handling of an unreferenced `ARG`, and
|
|
it puts the epoch in the build log. The guard is itself value-keyed, for
|
|
the same reason: it references `$CHECK_EPOCH`, so BuildKit renders the
|
|
epoch into that layer's description (rendered as
|
|
`RUN [ -n "<epoch>" ] || exit 1`) and re-runs it whenever the value
|
|
changes. Each stage therefore has two independent invalidation points, and
|
|
the guard always precedes the check `RUN`. Keep both: the expansion is
|
|
defence in depth, and it is what makes the epoch visible in the build
|
|
output.
|
|
- The `[ -n ... ]` guard is required: an unset `ARG` is empty, and empty is
|
|
a stable cache key, so without it a bare `docker build .` still produces
|
|
the false green. Failed steps are never cached, so the guard fails on
|
|
every such invocation, loudly. A bare `docker build .` failing is by
|
|
design.
|
|
- Assign `epoch=` on its own line, never inline in the `--build-arg`
|
|
argument: a failing command substitution inside an argument does not trip
|
|
`set -e`, so the inline form silently degrades to an empty constant. The
|
|
`$$` suffix is required because busybox `date` drops `%N` and exits 0, so
|
|
on an alpine host the epoch would degrade to second granularity and
|
|
concurrent invocations would collide.
|
|
|
|
This invalidates the check layers and everything after them while leaving
|
|
`go mod download`, `script/bootstrap`, and the pinned toolchain install
|
|
cached, so it does not push against the five-minute Docker build ceiling.
|
|
Blanket `--no-cache` also works but is wasteful and can blow that ceiling.
|
|
|
|
- **Dockerfiles must use a separate lint stage for fail-fast feedback.** Go
|
|
repos use a multistage build where linting runs in an independent stage based
|
|
on the `golangci/golangci-lint` image (pinned by hash). This stage runs
|
|
`make fmt-check` and `make lint` before the full build begins. The build stage
|
|
then declares an explicit dependency on the lint stage via
|
|
`COPY --from=lint /src/go.sum /dev/null`, which forces BuildKit to complete
|
|
linting before proceeding to compilation and tests. This ensures lint failures
|
|
surface in seconds rather than minutes, without blocking on dependency
|
|
download or compilation in the build stage.
|
|
|
|
The standard pattern for a Go repo Dockerfile is:
|
|
|
|
```dockerfile
|
|
# Lint stage — fast feedback on formatting and lint issues
|
|
# golangci/golangci-lint:v2.x.x, YYYY-MM-DD
|
|
FROM golangci/golangci-lint@sha256:... AS lint
|
|
WORKDIR /src
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
COPY . .
|
|
ARG CHECK_EPOCH
|
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make fmt-check
|
|
RUN make lint
|
|
|
|
# Build stage
|
|
# golang:1.x-alpine, YYYY-MM-DD
|
|
FROM golang@sha256:... AS builder
|
|
WORKDIR /src
|
|
|
|
# Force BuildKit to run the lint stage before proceeding
|
|
COPY --from=lint /src/go.sum /dev/null
|
|
|
|
COPY go.mod go.sum ./
|
|
RUN go mod download
|
|
COPY . .
|
|
ARG CHECK_EPOCH
|
|
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
|
|
|
|
# VERSION comes from the host via --build-arg; see the git-describe rule
|
|
# below. Never run `git describe` here: .dockerignore excludes .git, so
|
|
# it yields an empty version without failing the build.
|
|
ARG VERSION=dev
|
|
RUN CGO_ENABLED=0 go build -trimpath \
|
|
-ldflags="-s -w -X main.Version=${VERSION}" \
|
|
-o /app ./cmd/app/
|
|
|
|
# Runtime stage
|
|
FROM alpine@sha256:...
|
|
COPY --from=builder /app /usr/local/bin/app
|
|
ENTRYPOINT ["app"]
|
|
```
|
|
|
|
Key points:
|
|
- The lint stage uses the `golangci/golangci-lint` image directly (it
|
|
includes both Go and the linter), so there is no need to install the
|
|
linter separately.
|
|
- `COPY --from=lint /src/go.sum /dev/null` is a no-op file copy that creates
|
|
a stage dependency. BuildKit runs stages in parallel by default; without
|
|
this line, the build stage would not wait for lint to finish and a lint
|
|
failure might not fail the overall build.
|
|
- **Re-prove that ordering on a warm cache after adopting `CHECK_EPOCH`.**
|
|
The cache-bust turns this no-op `COPY` into a content-cache hit, so an
|
|
ordering guarantee established on a cold cache does not automatically
|
|
carry over; it has to be re-checked warm. This was re-proved in another
|
|
repo in the org that uses the same file-dependency trick (there with a
|
|
marker file in place of `go.sum`), and the ordering held. It has **not**
|
|
been verified in this repo, which is single-stage and has no lint stage to
|
|
order against. Any repo relying on a file-dependency trick for stage
|
|
ordering should re-check it warm after adopting the bust rather than
|
|
assuming this result transfers.
|
|
- If the project uses `//go:embed` directives that reference build artifacts
|
|
(e.g. a web frontend compiled in a separate stage), the lint stage must
|
|
create placeholder files so the embed directives resolve. Example:
|
|
`RUN mkdir -p web/dist && touch web/dist/index.html web/dist/style.css`.
|
|
The lint stage should not depend on the actual build output — it exists to
|
|
fail fast.
|
|
- If the project requires CGO or system libraries for linting (e.g.
|
|
`vips-dev`), install them in the lint stage with `apk add`.
|
|
- The build stage runs `make test` after compilation setup. Tests run in the
|
|
build stage, not the lint stage, because they may require compiled
|
|
artifacts or heavier dependencies.
|
|
- `ARG CHECK_EPOCH` appears in **both** stages, because `ARG` is
|
|
stage-scoped: declaring it only in the lint stage leaves `make test`
|
|
frozen at the last cached result. In each stage the guard sits immediately
|
|
below the `ARG` so a bare `docker build .` fails instead of reusing the
|
|
empty cache key, and the value is expanded into the first check `RUN` so
|
|
the cache miss does not rely on BuildKit's unreferenced-`ARG` handling.
|
|
Both of those lines reference `$CHECK_EPOCH`, so both are value-keyed:
|
|
each stage is invalidated at two independent points. The later `RUN`s in
|
|
the same stage need no expansion of their own: they are already
|
|
invalidated by their busted parent layer.
|
|
- `ARG VERSION=dev` is declared in the build stage, and its value is
|
|
supplied on the host by `script/docker` and `script/cibuild` via
|
|
`--build-arg VERSION=...`. The `dev` default is a placeholder for a local
|
|
build, not a source of truth. **No stage may call `git describe`**:
|
|
`.dockerignore` excludes `.git`, so it yields an empty version without
|
|
failing. See the git-describe rule further down.
|
|
|
|
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
|
runs `script/cibuild` (which runs
|
|
`docker build --build-arg CHECK_EPOCH="$epoch" --build-arg VERSION="$version" .`)
|
|
on push. The Dockerfile runs `make check`, so a successful build implies all
|
|
checks pass — but that implication holds **only** because of the `CHECK_EPOCH`
|
|
cache-bust described above. Without it, an unchanged tree serves the check
|
|
layer from cache and the build reports a green it never earned. A bare
|
|
`docker build .` fails closed by design, on the `[ -n "$CHECK_EPOCH" ]` guard;
|
|
always go through `script/cibuild` or `script/docker`. Never accept a
|
|
`script/cibuild` pass as evidence without confirming it ran: a sub-second wall
|
|
time, or `CACHED` on the check layer, means nothing was executed.
|
|
|
|
- Use platform-standard formatters: `black` for Python, `prettier` for
|
|
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
|
two exceptions: four-space indents (except Go), and `proseWrap: always` for
|
|
Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown,
|
|
HTML, CSS) should also have `.prettierrc` and `.prettierignore`.
|
|
|
|
- Pre-commit hook: runs `script/precommit`, which calls `script/check`. If local
|
|
testing is not possible in the repo, `script/precommit` may skip `script/test`
|
|
and run only `script/lint` and `script/fmt-check`. The hook is installed by
|
|
`script/install-precommit`; the Makefile must provide a `make hooks` target
|
|
that shims to it.
|
|
|
|
- All repos with software must have tests that run via the platform-standard
|
|
test framework (`go test`, `pytest`, `jest`/`vitest`, etc.). If no meaningful
|
|
tests exist yet, add the most minimal test possible — e.g. importing the
|
|
module under test to verify it compiles/parses. There is no excuse for
|
|
`make test` to be a no-op.
|
|
|
|
- `make test` must complete in under 60 seconds. That is the hard cap, and a
|
|
suite that exceeds it fails. Under 20 seconds is the target. A suite between
|
|
20 and 60 seconds is still green, but the overage must be filed as an
|
|
improvement bug against that repo. Add a 90-second timeout to the test
|
|
invocation in the Makefile (`go test -timeout 90s`). The backstop deliberately
|
|
sits above the hard cap so that it catches a genuinely hung test rather than a
|
|
merely slow one.
|
|
|
|
- **`make test` should use the conditional verbose rerun pattern.** Run tests
|
|
without `-v` (verbose) first. If tests fail, automatically rerun with `-v` to
|
|
show full output. This keeps CI logs and `docker build` output clean on
|
|
success (just package/suite summaries) while providing full diagnostic detail
|
|
on failure (every test case, every assertion). The general shell pattern:
|
|
|
|
```makefile
|
|
test:
|
|
@<test-command> || \
|
|
{ echo "--- Rerunning with -v for details ---"; \
|
|
<test-command-with-v>; exit 1; }
|
|
```
|
|
|
|
Go example:
|
|
|
|
```makefile
|
|
test:
|
|
@go test -timeout 90s -race -cover ./... || \
|
|
{ echo "--- Rerunning with -v for details ---"; \
|
|
go test -timeout 90s -race -v ./...; exit 1; }
|
|
```
|
|
|
|
Python example:
|
|
|
|
```makefile
|
|
test:
|
|
@python -m pytest || \
|
|
{ echo "--- Rerunning with -v for details ---"; \
|
|
python -m pytest -v; exit 1; }
|
|
```
|
|
|
|
The `exit 1` ensures the target always fails after a rerun — the first run
|
|
already proved the tests are broken, so the build must not pass even if a
|
|
flaky test happens to succeed on the second attempt. The rerun exists solely
|
|
for diagnostic output.
|
|
|
|
- Docker builds must complete in under 5 minutes.
|
|
|
|
- `make check` must not modify any files in the repo. Tests may use temporary
|
|
directories.
|
|
|
|
- `main` must always pass `make check`, no exceptions.
|
|
|
|
- Never commit secrets. `.env` files, credentials, API keys, and private keys
|
|
must be in `.gitignore`. No exceptions.
|
|
|
|
- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`),
|
|
editor files (`.swp`, `*~`), in-repo agent scratch directories (`.claude/`,
|
|
which holds one worktree — an entire additional checkout of the repo — per
|
|
in-flight agent), language build artifacts, and `node_modules/`. Fetch the
|
|
standard `.gitignore` from
|
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
|
|
a new repo. These patterns are written to `.gitignore`'s own semantics, in
|
|
which an unanchored pattern already matches at every depth. They are not a
|
|
`.dockerignore` and must not be transplanted into one unmodified — see the
|
|
next rule.
|
|
|
|
- **`.dockerignore` does not use `.gitignore` semantics, and copying patterns
|
|
across unmodified leaves secrets in the build context.** Docker matches with
|
|
`moby/patternmatcher`: Go `filepath.Match` semantics plus a `**` extension,
|
|
compiled to a regexp — plain `filepath.Match` has no `**` at all. So `*` does
|
|
not cross `/`, and a pattern without a leading `**/` is anchored at the
|
|
build-context root. A `.dockerignore` listing `.env`, `*.pem` and `*.key`
|
|
therefore excludes only the copies at the repository root; `config/.env` and
|
|
`certs/server.key` still reach the context and can land in an image layer.
|
|
That file is more dangerous than a short one with no secret patterns at all,
|
|
because it reads as solved and stops anyone looking. Give every
|
|
depth-independent pattern the `**/` prefix — `**/node_modules`,
|
|
`**/.DS_Store`, and the secret patterns in the canonical file, which are
|
|
additionally case-folded per the rule below — and leave only genuinely
|
|
root-anchored entries unprefixed: `.git`, the in-repo agent scratch directory
|
|
`.claude`, and the repo's own host-built binary. The inverse move is equally
|
|
wrong: never apply `**/` to `.gitignore`, where it is redundant and produces a
|
|
file that is wrong in a way that looks careful. Each file is written to its
|
|
own semantics; neither is derived from the other. Fetch the standard
|
|
`.dockerignore` from
|
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.dockerignore` and extend
|
|
it with the repo's own host-built artifacts — a host `make build` that leaves
|
|
a compiled binary in the repo root puts that binary in the build context,
|
|
where `.gitignore` hides it from every git-based check. Write that binary
|
|
anchored, `/myapp` and never `**/myapp`: the prefixed form also matches
|
|
`cmd/myapp/` and deletes the package directory from the context.
|
|
|
|
- **In-repo agent scratch belongs in both files, written to each file's own
|
|
semantics.** `.claude/` holds one worktree per in-flight agent — an entire
|
|
additional checkout of the repo — so with `COPY . .` the build context
|
|
inflates by a multiple of the repo, and another session's unreviewed,
|
|
sometimes uncommitted work can be copied into an image layer. The directory is
|
|
also created and destroyed constantly, so it invalidates `COPY . .` for
|
|
reasons that have nothing to do with the repo's own content. In `.gitignore`
|
|
the entry is `.claude/`, unanchored, which already matches at every depth. In
|
|
`.dockerignore` it is `.claude`, anchored and with **no** `**/` prefix: the
|
|
directory occurs exactly once **where agents run at the repo root**, and the
|
|
prefixed form would also match any nested directory of that name and delete it
|
|
from the build. It is not case-folded the way the secret patterns are, because
|
|
tooling creates it in exactly one spelling, so a folded pattern would add no
|
|
coverage.
|
|
|
|
**Known gap that comes with the anchored form.** The directory is created in
|
|
the agent's working directory, so the "exactly once, at the root" premise is
|
|
a property of how agents are run and not of the tooling. Where agents run in
|
|
subdirectories — a monorepo with a per-service agent is the ordinary case —
|
|
`services/api/.claude/` is **not** excluded by the canonical entry and still
|
|
reaches the build context and the image, which is the exposure the entry
|
|
exists to close. A repo in that shape adds its own anchored entries
|
|
(`/services/api/.claude`), or `**/.claude` once it has confirmed no
|
|
legitimately named nested directory would be caught. This is stated in the
|
|
canonical `.dockerignore` itself, since that file is what consuming repos
|
|
receive.
|
|
|
|
- **`.dockerignore` matching is case-sensitive, so cover capitalisation with
|
|
character classes rather than by doubling patterns.** `**/*.key` does not
|
|
match `certs/SERVER.KEY`, which is reachable on the case-insensitive
|
|
filesystems most laptops use. Adding an ALL-CAPS twin for each pattern is not
|
|
the fix: it still misses `Server.Key` and `Ca.Pem` while reading as though
|
|
case were handled — the same manufactured confidence as the root-anchored
|
|
form. The matcher supports character ranges, so one line covers every
|
|
spelling: `**/*.[kK][eE][yY]`, `**/*.[pP][eE][mM]`. Apply this to every secret
|
|
name, not only to extensions: the extensionless SSH keys and `.envrc` need it
|
|
for the same reason, since on the very filesystems that make `SERVER.KEY`
|
|
reachable, direnv reads `.ENVRC` and ssh reads `ID_RSA`. Note that `*` matches
|
|
the empty string, so `**/*.[eE][nN][vV]` already covers a bare `.ENV` and no
|
|
separate literal `.env` entry is needed.
|
|
|
|
- **A pattern that also catches something the build needs is re-included with a
|
|
negation, not deleted.** The canonical `**/*.[eE][nN][vV]` excludes a
|
|
committed env template such as `example.env`; a repo whose build genuinely
|
|
reads one adds `!docs/example.env` after the pattern. Deleting the pattern
|
|
instead reopens the exposure for every other file it covers.
|
|
|
|
- **Verify `.dockerignore` by enumerating the image, not by reading the
|
|
patterns.** Plant files at the root _and_ at least two directories deep, build
|
|
a probe image that does `COPY . .`, and list what actually landed
|
|
(`docker run --rm --entrypoint find IMAGE /app`). Reading the patterns and
|
|
agreeing they look right is exactly what lets the root-only form through. The
|
|
`transferring context` size is not a substitute: a nested secret is a few
|
|
bytes, and BuildKit transfers only the delta from the previous build, so the
|
|
reported size describes the transfer and not the contents of the image.
|
|
|
|
- **Excluding `.git` means `git describe` cannot run inside any build stage, and
|
|
it fails quietly there.** The `GOLDFLAGS` version-embedding pattern assumes
|
|
`.git` is present; in a build stage there is no repository, so `git describe`
|
|
writes nothing to stdout and the `-X main.Version=` value comes out **empty**
|
|
rather than erroring. The binary then reports no version at all and the build
|
|
still exits 0. Compute the version **on the host** and thread it in as a build
|
|
arg. `script/docker` and `script/cibuild` do this, byte-identically across
|
|
repos:
|
|
|
|
```sh
|
|
# Assign on its own line: a failing command substitution inside an
|
|
# argument does not trip `set -e`, so the inline form degrades to an
|
|
# empty constant — the same silent-empty failure this rule is about.
|
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
|
[ -n "$version" ] || version="unknown"
|
|
docker build \
|
|
--build-arg CHECK_EPOCH="$epoch" \
|
|
--build-arg VERSION="$version" \
|
|
.
|
|
```
|
|
|
|
`--always` makes an untagged repo yield the abbreviated commit hash instead
|
|
of failing. `|| true` keeps a failing `git describe` from tripping `set -e`
|
|
and leaves the value empty, so the `[ -n "$version" ]` line is the single
|
|
place the fallback is applied — and it is a **live** check, not defence in
|
|
depth: it fires on a build from an export with no `.git`, and on a
|
|
repository with no commits yet. Do not fold the fallback into the
|
|
substitution as `|| echo unknown`; that makes the guard unreachable, and a
|
|
guard that cannot fire is indistinguishable from one that works to everyone
|
|
who copies it. The result is non-empty by construction either way, which is
|
|
the point: an empty version reads as a successful one, while `unknown` is
|
|
visibly wrong. The Dockerfile's side is `ARG VERSION=dev` in the stage that
|
|
compiles, declared there and not inherited, because `ARG` is stage-scoped
|
|
exactly as `CHECK_EPOCH` is. Passing `VERSION` to a repo whose Dockerfile
|
|
declares no such `ARG` is silently ignored by BuildKit and costs nothing,
|
|
which is why the scripts stay byte-identical rather than growing a per-repo
|
|
variant.
|
|
|
|
One consequence for CI: the standard checkout action clones shallow and
|
|
fetches no tags, so `git describe --tags` there falls back to a bare commit
|
|
hash. A repo that embeds a tag-derived version must set `fetch-depth: 0` on
|
|
its checkout step; a repo that does not embed a version needs no change.
|
|
|
|
- **No build artifacts in version control.** Code-derived data (compiled
|
|
bundles, minified output, generated assets) must never be committed to the
|
|
repository if it can be avoided. The build process (e.g. Dockerfile, Makefile)
|
|
should generate these at build time. Notable exception: Go protobuf generated
|
|
files (`.pb.go`) ARE committed because repos need to work with `go get`, which
|
|
downloads code but does not execute code generation.
|
|
|
|
- Never use `git add -A` or `git add .`. Always stage files explicitly by name.
|
|
|
|
- Never force-push to `main`.
|
|
|
|
- Make all changes on a feature branch. You can do whatever you want on a
|
|
feature branch.
|
|
|
|
- `.golangci.yml` is standardized. The vendored copy in a consuming repo must
|
|
_NEVER_ be modified by an agent: fetch it from
|
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml` and keep it
|
|
byte-identical, so that no repo can quietly loosen its own linting. Linter
|
|
configuration changes are made to the canonical copy in the `prompts` repo and
|
|
reach consuming repos by re-vendoring; an agent may open a PR against
|
|
canonical, which only the user merges. One list is exempt from byte-identity,
|
|
because it cannot be written once for every repo: the `deny` list of the
|
|
`test-support` depguard rule, where a repo names its own test-support packages
|
|
by full import path. A repo adds entries there and changes nothing else, and a
|
|
re-vendor carries its entries forward. The canonical golangci-lint version is
|
|
v2.12.2 (released 2026-05-06), installed commit-pinned via
|
|
`go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5`.
|
|
|
|
- **`script/bootstrap` in Go repos must install the pinned golangci-lint
|
|
whenever the installed version does not match the pin — not merely when the
|
|
binary is absent — and must then verify the install took effect by
|
|
re-resolving the binary through `PATH`.** The presence test
|
|
`if missing golangci-lint; then go install "$GOLANGCI_LINT_REF"; fi` is wrong:
|
|
it tests `PATH` presence and never version, so on any already-provisioned
|
|
machine the pin is inert and a version bump is a no-op. Meanwhile the
|
|
Dockerfile installs unconditionally into a clean image, so CI and local
|
|
silently disagree about what the linter even is. Observed consequences: a
|
|
local `make check` green while `make docker` rejected the same commit with six
|
|
`goconst` findings, and a container linter surfacing thirteen findings the
|
|
host run missed. A stale host linter does not merely fail to prove the tree is
|
|
clean — it hides findings only the container can see. This is a deliberate
|
|
departure from the node handling described above, which uses whatever node is
|
|
installed: the linter version is the specific thing being held equal between
|
|
host and container, so for it, presence is not enough.
|
|
|
|
Comparing versions is necessary but **not sufficient**, because the obvious
|
|
fix also fails green. `go install` writes to `GOBIN` (or `GOPATH/bin`) while
|
|
callers resolve `golangci-lint` through `PATH`. If a different binary
|
|
shadows it earlier in `PATH`, the install genuinely succeeds and changes
|
|
nothing any caller will ever see: bootstrap prints success and the next
|
|
`make lint` still runs the stale linter. That is worse than no fix, because
|
|
it converts a known-stale toolchain into one everyone believes is pinned.
|
|
The canonical form, placed in `script/bootstrap` after Go itself is present:
|
|
|
|
```sh
|
|
# golangci-lint v2.12.2, 2026-05-06. GOLANGCI_LINT_VERSION must be exactly
|
|
# what `golangci-lint --version` prints for this ref; update both together.
|
|
GOLANGCI_LINT_VERSION="2.12.2"
|
|
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5"
|
|
|
|
# The version golangci-lint reports, resolved the way callers resolve it.
|
|
# Prints nothing when the binary is absent, exits non-zero, or prints
|
|
# something unparseable: all of those must read as "does not match".
|
|
# The capture is the whole version token, not just its numeric prefix.
|
|
# Stopping at the first `-` would make 2.12.2-rc1 compare equal to 2.12.2
|
|
# and skip the install, which is the defect this whole rule exists to close.
|
|
# The trailing `|| true` is required, not tidiness. Under `set -o pipefail`
|
|
# a non-zero --version would otherwise propagate out of the pipeline and
|
|
# kill the script through `set -e` before the diagnostic below is printed.
|
|
golangci_lint_version() {
|
|
command -v golangci-lint >/dev/null 2>&1 || return 0
|
|
golangci-lint --version 2>/dev/null | head -n 1 |
|
|
sed -n 's/.*has version v\{0,1\}\([0-9][^ ]*\).*/\1/p' || true
|
|
}
|
|
|
|
ensure_golangci_lint() {
|
|
if [ "$(golangci_lint_version)" = "$GOLANGCI_LINT_VERSION" ]; then
|
|
echo "bootstrap: golangci-lint $GOLANGCI_LINT_VERSION already installed"
|
|
return 0
|
|
fi
|
|
echo "bootstrap: installing golangci-lint $GOLANGCI_LINT_VERSION"
|
|
go install "$GOLANGCI_LINT_REF"
|
|
|
|
# go install writes to GOBIN (or GOPATH/bin); callers resolve through
|
|
# PATH. Re-resolve through PATH and assert the install took effect.
|
|
# `hash -r` is load-bearing: without it a shell that already resolved
|
|
# a stale golangci-lint answers from its own lookup cache, and this
|
|
# check false-fails with the shadowing message below.
|
|
hash -r 2>/dev/null || true
|
|
gcl_got="$(golangci_lint_version)"
|
|
if [ "$gcl_got" = "$GOLANGCI_LINT_VERSION" ]; then
|
|
echo "bootstrap: golangci-lint $GOLANGCI_LINT_VERSION installed," \
|
|
"and PATH resolves it"
|
|
return 0
|
|
fi
|
|
gcl_bin="$(go env GOBIN)"
|
|
[ -n "$gcl_bin" ] || gcl_bin="$(go env GOPATH)/bin"
|
|
# Strip a trailing slash: GOBIN=/x/ would otherwise make the
|
|
# "$gcl_bin"/* test below miss and misreport shadowing.
|
|
while :; do
|
|
case "$gcl_bin" in
|
|
*/) gcl_bin="${gcl_bin%/}" ;;
|
|
*) break ;;
|
|
esac
|
|
done
|
|
gcl_found="$(command -v golangci-lint 2>/dev/null || true)"
|
|
echo "bootstrap: installed golangci-lint $GOLANGCI_LINT_VERSION into" \
|
|
"$gcl_bin, but that is not what callers will get." >&2
|
|
case "$gcl_found" in
|
|
"")
|
|
echo "bootstrap: PATH resolves no golangci-lint at all." \
|
|
"Add $gcl_bin to PATH, then re-run bootstrap." >&2
|
|
;;
|
|
"$gcl_bin"/*)
|
|
echo "bootstrap: PATH resolves $gcl_found, inside that same" \
|
|
"directory, reporting version ${gcl_got:-unparseable}." \
|
|
"Nothing is shadowing it, so the install itself did not" \
|
|
"produce the pinned version: check that" \
|
|
"GOLANGCI_LINT_VERSION matches GOLANGCI_LINT_REF." >&2
|
|
;;
|
|
*)
|
|
echo "bootstrap: PATH resolves $gcl_found instead, reporting" \
|
|
"version ${gcl_got:-unparseable}. Remove that binary or" \
|
|
"put $gcl_bin earlier in PATH, then re-run bootstrap." >&2
|
|
;;
|
|
esac
|
|
exit 1
|
|
}
|
|
|
|
# The definitions above are inert on their own; the call site is part of
|
|
# the canonical form. In a script/bootstrap that follows the "define all
|
|
# functions, then call main" convention, this line belongs inside main()
|
|
# next to the other ensure_* steps.
|
|
ensure_golangci_lint
|
|
```
|
|
|
|
Four properties are load-bearing; each guards a failure mode that otherwise
|
|
fails green:
|
|
- **Compare the installed version against the pin**, never test presence.
|
|
This is what makes a version bump propagate to machines that already have
|
|
some golangci-lint. Compare the **whole** version token, exactly: a parser
|
|
that stops at the first `-` reports `2.12.2` for a host running
|
|
`2.12.2-rc1`, which compares equal to a `2.12.2` pin and skips the install
|
|
— the original defect, reintroduced through the comparison meant to fix
|
|
it.
|
|
- **After installing, re-resolve the binary the way callers resolve it** —
|
|
through `PATH`, not the path `go install` wrote to — and assert
|
|
`--version` reports the pin. When it does not, fail non-zero and name the
|
|
path `command -v` actually found, the version it reports, and the
|
|
directory the install wrote to. That is a condition a human has to fix by
|
|
hand, so bootstrap must not print success in it. Use `hash -r` first so
|
|
the shell does not answer from its own lookup cache. Diagnose the cause
|
|
from the resolved path rather than asserting one: only a path **outside**
|
|
the install directory is shadowing. When the resolved path is inside it,
|
|
nothing is shadowing and telling the operator to delete that binary or
|
|
reorder `PATH` sends them after a fault that does not exist.
|
|
- **A mis-parse must fall through to reinstall, never to a false match.**
|
|
Absent binary, non-zero exit, empty output, and unrecognised output all
|
|
yield an empty string, which compares unequal to the pin. The failure
|
|
direction is always a redundant install, never a skipped one.
|
|
- **Call it, and say so on success.** Two function definitions with no call
|
|
site are a silent no-op that reproduces the original defect exactly: exit
|
|
0, nothing installed, no output, stale linter still resolved. A success
|
|
path that prints nothing is byte-identical to that no-op — same exit
|
|
status, same empty output — so both success branches must print a
|
|
confirmation naming the version. In a change about undetectable no-ops,
|
|
"it printed nothing and exited 0" must not be the healthy signal.
|
|
|
|
Keep it POSIX sh: no bashisms, no arrays, no `[[`, no `grep -P`.
|
|
|
|
**On the hash-pinning rule.** `@c0d3ddc9cf3faa61a4e378e879ece580256d76e5` is
|
|
a commit hash, not a server-mutable version tag, and the go command verifies
|
|
the fetched module against the checksum database — the mechanism the
|
|
hash-pinning rule at the top of this document already names as acceptable
|
|
for Go modules. Note that `go install pkg@version` runs in module-aware mode
|
|
ignoring the `go.mod` in the current directory or any parent, so no repo
|
|
`go.sum` is consulted for this install; the checksum database is what
|
|
verifies it. The linter is a bootstrap prerequisite rather than part of any
|
|
repo's module graph, which is why the canonical form installs it by
|
|
commit-pinned ref instead of declaring it in `go.mod`. Whether a `go.mod`
|
|
tool dependency — which would pin the hash in a committed, reviewable file
|
|
instead — should replace this is an open decision, tracked at
|
|
[prompts#37](https://git.eeqj.de/sneak/prompts/issues/37).
|
|
|
|
**Keep `GOLANGCI_LINT_VERSION` and the ref in sync.** The ref is a hash and
|
|
carries no readable version, so the expected version is a separate string,
|
|
and it must be exactly what `--version` prints for that ref — the comparison
|
|
is an exact match on the whole version token. When the pinned commit carries
|
|
a release tag the go command resolves the hash to that tag, so the string is
|
|
simply the release number, `2.12.2` here. When it does not, the go command
|
|
falls back to a pseudo-version and the binary reports something like
|
|
`2.12.3-0.20260506110758-c0d3ddc9cf3f`; that compares exactly like any other
|
|
string, so it works, but it cannot be known without building the binary once
|
|
and reading `--version` off it. Prefer pins on tagged releases for that
|
|
reason — the expected string is then derivable from the ref — not because
|
|
the comparison cannot handle the alternative.
|
|
|
|
Because the comparison covers the whole token, a pre-release is never
|
|
confused with its release: a host carrying `2.12.2-rc1` against a `2.12.2`
|
|
pin compares unequal and gets reinstalled. This matters more than it looks,
|
|
because a pre-release tag is still a tag, so a rule requiring merely that
|
|
the pin be tagged would not catch it.
|
|
|
|
**Verifying a change to this logic requires a negative control run in an
|
|
environment where a shadowing binary exists earlier in `PATH` than the
|
|
install target.** Without that, the control passes against the naive
|
|
compare-then-install form as well and therefore proves nothing. Also check
|
|
the mis-parse direction by feeding it unparseable `--version` output and
|
|
confirming it reinstalls rather than reporting a match.
|
|
|
|
**Run those controls against the block as a consuming repo would adopt it**
|
|
— pasted into a `script/bootstrap`-shaped file that is then executed — not
|
|
by sourcing it and invoking the function yourself. Driving the function
|
|
directly tests something the artifact does not do, and it is exactly how a
|
|
missing call site passes every control while the adopted snippet does
|
|
nothing.
|
|
|
|
- When pinning images or packages by hash, add a comment above the reference
|
|
with the version and date (YYYY-MM-DD).
|
|
|
|
- Use `yarn`, not `npm`.
|
|
|
|
- Write all dates as YYYY-MM-DD (ISO 8601).
|
|
|
|
- Simple projects should be configured with environment variables.
|
|
|
|
- Dockerized web services listen on port 8080 by default, overridable with
|
|
`PORT`.
|
|
|
|
- **HTTP/web services must be hardened for production internet exposure before
|
|
tagging 1.0.** This means full compliance with security best practices
|
|
including, without limitation, all of the following:
|
|
- **Security headers** on every response:
|
|
- `Strict-Transport-Security` (HSTS) with `max-age` of at least one year
|
|
and `includeSubDomains`.
|
|
- `Content-Security-Policy` (CSP) with a restrictive default policy
|
|
(`default-src 'self'` as a baseline, tightened per-resource as
|
|
needed). Never use `unsafe-inline` or `unsafe-eval` unless
|
|
unavoidable, and document the reason.
|
|
- `X-Frame-Options: DENY` (or `SAMEORIGIN` if framing is required).
|
|
Prefer the `frame-ancestors` CSP directive as the primary control.
|
|
- `X-Content-Type-Options: nosniff`.
|
|
- `Referrer-Policy: strict-origin-when-cross-origin` (or stricter).
|
|
- `Permissions-Policy` restricting access to browser features the
|
|
application does not use (camera, microphone, geolocation, etc.).
|
|
- **Request and response limits:**
|
|
- Maximum request body size enforced on all endpoints (e.g. Go
|
|
`http.MaxBytesReader`). Choose a sane default per-route; never accept
|
|
unbounded input.
|
|
- Maximum response body size where applicable (e.g. paginated APIs).
|
|
- `ReadTimeout` and `ReadHeaderTimeout` on the `http.Server` to defend
|
|
against slowloris attacks.
|
|
- `WriteTimeout` on the `http.Server`.
|
|
- `IdleTimeout` on the `http.Server`.
|
|
- Per-handler execution time limits via `context.WithTimeout` or
|
|
chi/stdlib `middleware.Timeout`.
|
|
- **Authentication and session security:**
|
|
- Rate limiting on password-based authentication endpoints. API keys are
|
|
high-entropy and not susceptible to brute force, so they are exempt.
|
|
- CSRF tokens on all state-mutating HTML forms. API endpoints
|
|
authenticated via `Authorization` header (Bearer token, API key) are
|
|
exempt because the browser does not attach these automatically.
|
|
- Passwords stored using bcrypt, scrypt, or argon2 — never plain-text,
|
|
MD5, or SHA.
|
|
- Session cookies set with `HttpOnly`, `Secure`, and `SameSite=Lax` (or
|
|
`Strict`) attributes.
|
|
- **Reverse proxy awareness:**
|
|
- True client IP detection when behind a reverse proxy
|
|
(`X-Forwarded-For`, `X-Real-IP`). The application must accept
|
|
forwarded headers only from a configured set of trusted proxy
|
|
addresses — never trust `X-Forwarded-For` unconditionally.
|
|
- **CORS:**
|
|
- Authenticated endpoints must restrict `Access-Control-Allow-Origin` to
|
|
an explicit allowlist of known origins. Wildcard (`*`) is acceptable
|
|
only for public, unauthenticated read-only APIs.
|
|
- **Error handling:**
|
|
- Internal errors must never leak stack traces, SQL queries, file paths,
|
|
or other implementation details to the client. Return generic error
|
|
messages in production; detailed errors only when `DEBUG` is enabled.
|
|
- **TLS:**
|
|
- Services never terminate TLS directly. They are always deployed behind
|
|
a TLS-terminating reverse proxy. The service itself listens on plain
|
|
HTTP. However, HSTS headers and `Secure` cookie flags must still be
|
|
set by the application so that the browser enforces HTTPS end-to-end.
|
|
|
|
This list is non-exhaustive. Apply defense-in-depth: if a standard security
|
|
hardening measure exists for HTTP services and is not listed here, it is
|
|
still expected. When in doubt, harden.
|
|
|
|
- `README.md` is the primary documentation. Required sections:
|
|
- **Description**: First line must include the project name, purpose,
|
|
category (web server, SPA, CLI tool, etc.), license, and author. Example:
|
|
"µPaaS is an MIT-licensed Go web application by @sneak that receives
|
|
git-frontend webhooks and deploys applications via Docker in realtime."
|
|
- **Getting Started**: Copy-pasteable install/usage code block.
|
|
- **Entrypoints**: Opens by stating that the repo adheres to the
|
|
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
|
|
standard (with that link), then documents each provided `script/`
|
|
entrypoint and its purpose.
|
|
- **Rationale**: Why does this exist?
|
|
- **Design**: How is the program structured?
|
|
- **TODO**: Update meticulously, even between commits. When planning, put
|
|
the todo list in the README so a new agent can pick up where the last one
|
|
left off.
|
|
- **License**: MIT, GPL, or WTFPL. Ask the user for new projects. Include a
|
|
`LICENSE` file in the repo root and a License section in the README.
|
|
- **Author**: [@sneak](https://sneak.berlin).
|
|
|
|
- First commit of a new repo should contain only `README.md`.
|
|
|
|
- Go module root: `sneak.berlin/go/<name>`. Always run `go mod tidy` before
|
|
committing.
|
|
|
|
- Use SemVer.
|
|
|
|
- Database migrations live in `internal/db/migrations/` and must be embedded in
|
|
the binary.
|
|
- `000_migration.sql` — contains ONLY the creation of the migrations
|
|
tracking table itself. Nothing else.
|
|
- `001_schema.sql` — the full application schema.
|
|
- **Pre-1.0.0:** never add additional migration files (002, 003, etc.).
|
|
There is no installed base to migrate. Edit `001_schema.sql` directly.
|
|
- **Post-1.0.0:** add new numbered migration files for each schema change.
|
|
Never edit existing migrations after release.
|
|
|
|
- All repos should have an `.editorconfig` enforcing the project's indentation
|
|
settings.
|
|
|
|
- Avoid putting files in the repo root unless necessary. Root should contain
|
|
only project-level config files (`README.md`, `Makefile`, `Dockerfile`,
|
|
`LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and
|
|
language-specific config). Everything else goes in a subdirectory. Canonical
|
|
subdirectory names:
|
|
- `bin/` — executable scripts and tools
|
|
- `cmd/` — Go command entrypoints; thin only: one `main.go` per binary whose
|
|
body is a single call into `internal/` or `pkg/`, no project logic in
|
|
`cmd/`
|
|
- `configs/` — configuration templates and examples
|
|
- `deploy/` — deployment manifests (k8s, compose, terraform)
|
|
- `docs/` — documentation and markdown (README.md stays in root)
|
|
- `internal/` — Go internal packages
|
|
- `internal/db/migrations/` — database migrations
|
|
- `pkg/` — Go library packages
|
|
- `share/` — systemd units, data files
|
|
- `static/` — static assets (images, fonts, etc.)
|
|
- `web/` — web frontend source
|
|
|
|
- When setting up a new repo, files from the `prompts` repo may be used as
|
|
templates. Fetch them from
|
|
`https://git.eeqj.de/sneak/prompts/raw/branch/main/<path>`.
|
|
|
|
- New repos must contain at minimum:
|
|
- `README.md`, `.git`, `.gitignore`, `.editorconfig`
|
|
- `LICENSE`, `REPO_POLICIES.md` (copy from the `prompts` repo)
|
|
- `Makefile`
|
|
- `script/` entrypoints (`bootstrap`, `setup`, `projectname`, `test`,
|
|
`lint`, `fmt`, `fmt-check`, `check`, `docker`, `cibuild`, `precommit`,
|
|
`install-precommit`)
|
|
- `Dockerfile`, `.dockerignore`
|
|
- `.gitea/workflows/check.yml`
|
|
- Go: `go.mod`, `go.sum`, `.golangci.yml`
|
|
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
|
|
- Python: `pyproject.toml`
|