2 Commits

Author SHA1 Message Date
user
cedf98b83e rewrite first paragraph
All checks were successful
check / check (push) Successful in 5s
2026-03-04 14:41:58 -08:00
user
368fc0edf0 fix first paragraph
All checks were successful
check / check (push) Successful in 12s
2026-03-04 14:41:15 -08:00
3 changed files with 188 additions and 273 deletions

View File

@@ -1,6 +1,6 @@
---
title: Existing Repo Checklist
last_modified: 2026-03-10
last_modified: 2026-02-22
---
Use this checklist when beginning work in a repo that may not yet conform to our
@@ -78,22 +78,6 @@ with your task.
`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

View File

@@ -1,6 +1,8 @@
# LLM Prose Tells
A catalog of patterns found in LLM-generated prose.
Every pattern in this document shows up in human writing occasionally. They
become diagnostic only through density. A person might use one or two across an
entire essay, but LLM output packs fifteen into a single paragraph.
---
@@ -13,11 +15,19 @@ A negation followed by an em-dash and a reframe.
> "It's not just a tool—it's a paradigm shift." "This isn't about
> technology—it's about trust."
The single most recognizable LLM construction. Models produce this at roughly 10
to 50x the rate of human writers. Four of them in one essay and you know what
you're reading.
### Em-Dash Overuse Generally
Even outside the "not X but Y" pivot, models substitute em-dashes for commas,
semicolons, parentheses, colons, and periods. The em-dash can replace any other
punctuation mark, so models default to it.
Even outside the "not X but Y" pivot, models use em-dashes at far higher rates
than human writers. They substitute em-dashes for commas, semicolons,
parentheses, colons, and periods, often multiple times per paragraph. A human
writer might use one or two in an entire piece for a specific parenthetical
effect. Models scatter them everywhere because the em-dash can stand in for any
other punctuation mark, so they default to it. More than two or three per page
is a meaningful signal on its own.
### The Colon Elaboration
@@ -25,25 +35,33 @@ A short declarative clause, then a colon, then a longer explanation.
> "The answer is simple: we need to rethink our approach from the ground up."
Models reach for this in every other paragraph. The construction is perfectly
normal. The frequency gives it away.
### The Triple Construction
> "It's fast, it's scalable, and it's open source."
Three parallel items in a list, usually escalating. Always exactly three (rarely
two, never four) with strict grammatical parallelism.
two, never four) with strict grammatical parallelism that human writers rarely
bother maintaining.
### The Staccato Burst
> "This matters. It always has. And it always will." "The data is clear. The
> trend is undeniable. The conclusion is obvious."
Runs of very short sentences at the same cadence and matching length.
Runs of very short sentences at the same cadence. Human writers use a short
sentence for emphasis occasionally, but stacking three or four of them in a row
at matching length creates a mechanical regularity that reads as generated.
### The Two-Clause Compound Sentence
An independent clause, a comma, a conjunction ("and," "but," "which,"
"because"), and a second independent clause of similar length. Every sentence
becomes two balanced halves.
Possibly the most pervasive structural tell, and easy to miss because each
individual instance looks like normal English. The model produces sentence after
sentence where an independent clause is followed by a comma, a conjunction
("and," "but," "which," "because"), and a second independent clause of similar
length. Every sentence becomes two balanced halves joined in the middle.
> "The construction itself is perfectly normal, which is why the frequency is
> what gives it away." "They contain zero information, and the actual point
@@ -53,62 +71,76 @@ becomes two balanced halves.
Human prose has sentences with one clause, sentences with three, sentences that
start with a subordinate clause before reaching the main one, sentences that
embed their complexity in the middle.
embed their complexity in the middle. When every sentence on the page has that
same two-part structure, the rhythm becomes monotonous in a way that's hard to
pinpoint but easy to feel.
### Uniform Sentences Per Paragraph
Model-generated paragraphs contain between three and five sentences, a count
that holds steady across a piece. If the first paragraph has four sentences,
every subsequent paragraph will too.
Model-generated paragraphs contain between three and five sentences. This count
holds steady across an entire piece. If the first paragraph has four sentences,
every subsequent paragraph will too. Human writers are much more varied (a
single sentence followed by one that runs eight or nine) because they follow the
shape of an idea, not a template.
### The Dramatic Fragment
Sentence fragments used as standalone paragraphs for emphasis.
> "Full stop." "Let that sink in."
Sentence fragments used as standalone paragraphs for emphasis, like "Full stop."
or "Let that sink in." on their own line. Using one in an entire essay is a
reasonable stylistic choice, but models drop them in once per section or more,
at which point it becomes a habit rather than a deliberate decision.
### The Pivot Paragraph
> "But here's where it gets interesting." "Which raises an uncomfortable truth."
One-sentence paragraphs that exist only to transition between ideas, containing
zero information. The actual point is always in the next paragraph.
One-sentence paragraphs that exist only to transition between ideas. They
contain zero information. The actual point is always in the next paragraph.
Delete every one of these and the piece reads better.
### The Parenthetical Qualifier
> "This is, of course, a simplification." "There are, to be fair, exceptions."
Parenthetical asides inserted to perform nuance without changing the argument.
Parenthetical asides inserted to look thoughtful. The qualifier never changes
the argument that follows it. Its purpose is to perform nuance, not to express a
real reservation about what's being said.
### The Unnecessary Contrast
A contrasting clause appended to a statement that doesn't need one, using
"whereas," "as opposed to," "unlike," or "except that."
Models append a contrasting clause to statements that don't need one, tacking on
"whereas," "as opposed to," "unlike," or "except that" to draw a comparison the
reader could already infer.
> "Models write one register above where a human would, whereas human writers
> tend to match register to context."
The contrasting clause restates what the first clause already said. If you
delete the "whereas" clause and the sentence still says everything it needs to,
the contrast was filler.
The first clause already makes the point. The contrasting clause restates it
from the other direction. If you delete the "whereas" clause and the sentence
still says everything it needs to, the contrast was filler.
### Unnecessary Elaboration
Models keep going after the sentence has already made its point.
Models keep going after the sentence has already made its point, tacking on
clarifying phrases, adverbial modifiers, or restatements that add nothing.
> "A person might lean on one or two of these habits across an entire essay, but
> LLM output will use fifteen of them per paragraph, consistently, throughout
> the entire piece."
This sentence could end at "paragraph." The words after it repeat what "per
paragraph" already means. If you can cut the last third of a sentence without
losing meaning, the last third shouldn't be there.
This sentence could end at "paragraph." The words after it just repeat what "per
paragraph" already means. Models do this because they're optimizing for clarity
at the expense of concision, and because their training rewards thoroughness.
The result is prose that feels padded. If you can cut the last third of a
sentence without losing any meaning, the last third shouldn't be there.
### The Question-Then-Answer
> "So what does this mean for the average user? It means everything."
A rhetorical question immediately followed by its own answer.
A rhetorical question immediately followed by its own answer. Models do this two
or three times per piece because it fakes forward momentum. A human writer might
do it once.
---
@@ -116,38 +148,44 @@ A rhetorical question immediately followed by its own answer.
### Overused Intensifiers
"Crucial," "vital," "robust," "comprehensive," "fundamental," "arguably,"
The following words appear at dramatically elevated rates in model output:
"crucial," "vital," "robust," "comprehensive," "fundamental," "arguably,"
"straightforward," "noteworthy," "realm," "landscape," "leverage" (as a verb),
"delve," "tapestry," "multifaceted," "nuanced" (applied to the model's own
analysis), "pivotal," "unprecedented" (applied to things with plenty of
precedent), "navigate," "foster," "underscores," "resonates," "embark,"
"streamline," "spearhead."
"delve," "tapestry," "multifaceted," "nuanced" (which models apply to their own
analysis with startling regularity), "pivotal," "unprecedented" (frequently
applied to things with plenty of precedent), "navigate," "foster,"
"underscores," "resonates," "embark," "streamline," and "spearhead." Three or
more on the same page is a strong signal.
### Elevated Register Drift
Models write one register above where a human would, replacing "use" with
"utilize," "start" with "commence," "help" with "facilitate," "show" with
"demonstrate," "try" with "endeavor," "change" with "transform," and "make" with
"craft."
Models write one register above where a human would. "Use" becomes "utilize."
"Start" becomes "commence." "Help" becomes "facilitate." "Show" becomes
"demonstrate." "Try" becomes "endeavor." "Change" becomes "transform." "Make"
becomes "craft." The tendency holds regardless of topic or audience.
### Filler Adverbs
"Importantly," "essentially," "fundamentally," "ultimately," "inherently,"
"particularly," "increasingly." Dropped in to signal that something matters when
the writing itself should make the importance clear.
"particularly," "increasingly." Dropped in to signal that something matters,
which is unnecessary when the writing itself already makes the importance clear.
### The "Almost" Hedge
Instead of saying a pattern "always" or "never" does something, models write
"almost always," "almost never," "almost certainly," "almost exclusively." A
micro-hedge, less obvious than the full hedge stack.
Models rarely commit to an unqualified statement. Instead of saying a pattern
"always" or "never" does something, they write "almost always," "almost never,"
"almost certainly," "almost exclusively." The word "almost" shows up at
extraordinary density in model-generated analytical prose. It's a micro-hedge,
less obvious than the full hedge stack but just as diagnostic when it appears
ten or fifteen times in a single document.
### "In an era of..."
> "In an era of rapid technological change..."
Used to open an essay. The model is stalling while it figures out what the
actual argument is.
A model habit as an essay opener. The model uses it to stall while it figures
out what the actual argument is. Human writers don't begin a piece by zooming
out to the civilizational scale before they've said anything specific.
---
@@ -158,20 +196,24 @@ actual argument is.
> "While X has its drawbacks, it also offers significant benefits."
Every argument followed by a concession, every criticism softened. A direct
artifact of RLHF training, which penalizes strong stances.
artifact of RLHF training, which penalizes strong stances. Models reflexively
both-sides everything even when a clear position would serve the reader better.
### The Throat-Clearing Opener
> "In today's rapidly evolving digital landscape, the question of data privacy
> has never been more important."
The first paragraph adds no information. Delete it and the piece improves.
The first paragraph of most model-generated essays adds no information. Delete
it and the piece improves immediately. The actual argument starts in paragraph
two.
### The False Conclusion
> "At the end of the day, what matters most is..." "Moving forward, we must..."
The high school "In conclusion,..." dressed up for a professional audience.
Signals that the model is wrapping up without actually landing on anything.
### The Sycophantic Frame
@@ -192,13 +234,15 @@ the key considerations:"
> cases it can potentially offer significant benefits."
Five hedges in one sentence ("worth noting," "while," "may not be," "in many
cases," "can potentially"), communicating nothing.
cases," "can potentially"), communicating nothing. The model would rather be
vague than risk being wrong about anything.
### The Empathy Performance
> "This can be a deeply challenging experience." "Your feelings are valid."
Generic emotional language that could apply to anything.
Generic emotional language that could apply equally to a bad day at work or a
natural disaster. That interchangeability is what makes it identifiable.
---
@@ -206,28 +250,35 @@ Generic emotional language that could apply to anything.
### Symmetrical Section Length
If the first section runs about 150 words, every subsequent section will fall
between 130 and 170.
If the first section of a model-generated essay runs about 150 words, every
subsequent section will fall between 130 and 170. Human writing is much more
uneven, with 50 words in one section and 400 in the next.
### The Five-Paragraph Prison
Model essays follow a rigid introduction-body-conclusion arc even when nobody
asked for one. The introduction previews the argument, the body presents 3 to 5
points, the conclusion restates the thesis.
points, and then the conclusion restates the thesis using slightly different
words.
### Connector Addiction
The first word of each paragraph forms an unbroken chain of transition words:
"However," "Furthermore," "Moreover," "Additionally," "That said," "To that
end," "With that in mind," "Building on this."
Look at the first word of each paragraph in model output. You'll find an
unbroken chain of transition words: "However," "Furthermore," "Moreover,"
"Additionally," "That said," "To that end," "With that in mind," "Building on
this." Human prose moves between ideas without announcing every transition.
### Absence of Mess
Model prose doesn't contradict itself mid-paragraph and then catch the
contradiction, go on a tangent and have to walk it back, use an obscure idiom
without explaining it, make a joke that risks falling flat, leave a thought
genuinely unfinished, or keep a sentence the writer liked the sound of even
though it doesn't quite work.
contradiction. It doesn't go on a tangent and have to walk it back, use an
obscure idiom without explaining it, make a joke that risks falling flat, leave
a thought genuinely unfinished, or keep a sentence the writer liked the sound of
even though it doesn't quite work.
Human writing does all of those things regularly. That total absence of rough
patches and false starts is one of the strongest signals that text was
machine-generated.
---
@@ -237,27 +288,45 @@ though it doesn't quite work.
> "This has implications far beyond just the tech industry."
Zooming out to claim broader significance without substantiating it.
Zooming out to claim broader significance without substantiating it. The model
has learned that essays are supposed to gesture at big ideas, so it gestures.
Nothing concrete is behind the gesture.
### "It's important to note that..."
This phrase and its variants ("it's worth noting," "it bears mentioning," "it
should be noted") function as verbal tics before a qualification the model
believes someone expects.
should be noted") appear at absurd rates in model output. They function as
verbal tics before a qualification the model believes someone expects.
### The Metaphor Crutch
Models rely on a small, predictable set of metaphors: "double-edged sword," "tip
Models rely on a small, predictable set of metaphors ("double-edged sword," "tip
of the iceberg," "north star," "building blocks," "elephant in the room,"
"perfect storm," "game-changer."
"perfect storm," "game-changer") and reach for them with unusual regularity
across every topic. The pool is noticeably smaller than what human writers draw
from.
---
## How to Actually Spot It
No single pattern on this list proves anything by itself. Humans use em-dashes.
Humans write "crucial." Humans ask rhetorical questions.
What gives it away is how many of these show up at once. Model output will hit
10 to 20 of these patterns per page. Human writing might trigger 2 or 3,
distributed unevenly, mixed with idiosyncratic constructions no model would
produce. When every paragraph on the page reads like it came from the same
careful, balanced, slightly formal, structurally predictable process, it was
generated by one.
---
## Copyediting Checklist: Removing LLM Tells
Follow this checklist when editing any document to remove machine-generated
patterns. Do at least two full passes, because fixing one pattern often
introduces another.
patterns. Go through the entire list for every piece. Do at least two full
passes, because fixing one pattern often introduces another.
### Pass 1: Word-Level Cleanup
@@ -271,7 +340,8 @@ introduces another.
2. Search for filler adverbs ("importantly," "essentially," "fundamentally,"
"ultimately," "inherently," "particularly," "increasingly") and delete every
instance where the sentence still makes sense without it.
instance where the sentence still makes sense without it. That will be most
of them.
3. Look for elevated register drift ("utilize," "commence," "facilitate,"
"demonstrate," "endeavor," "transform," "craft" and similar) and replace with
@@ -279,6 +349,7 @@ introduces another.
4. Search for "it's important to note," "it's worth noting," "it bears
mentioning," and "it should be noted" and delete the phrase in every case.
The sentence that follows always stands on its own.
5. Search for the stock metaphors ("double-edged sword," "tip of the iceberg,"
"north star," "building blocks," "elephant in the room," "perfect storm,"
@@ -287,114 +358,101 @@ introduces another.
6. Search for "almost" used as a hedge ("almost always," "almost never," "almost
certainly," "almost exclusively") and decide in each case whether to commit
to the unqualified claim or to drop the sentence entirely.
to the unqualified claim or to drop the sentence entirely. If the claim needs
"almost" to be true, it might not be worth making.
7. Search for em-dashes and replace each one with the punctuation mark that
would normally be used in that position (comma, semicolon, colon, period, or
parentheses). If you can't identify which one it should be, the sentence
needs to be restructured.
8. Remove redundant adjectives. For each adjective, ask whether the sentence
changes meaning without it. "A single paragraph" means the same as "a
paragraph." "An entire essay" means the same as "an essay." If the adjective
doesn't change the meaning, cut it.
9. Remove unnecessary trailing clauses. Read the end of each sentence and ask
whether the last clause restates what the sentence already said. If so, end
the sentence earlier.
### Pass 2: Sentence-Level Restructuring
10. Find every em-dash pivot ("not Xbut Y," "not just XY," "more than X—Y")
and rewrite it as two separate clauses or a single sentence that makes the
point without the negation-then-correction structure.
8. Find every em-dash pivot ("not X...but Y," "not just X...Y," "more than
X...Y") and rewrite it as two separate clauses or a single sentence that
makes the point without the negation-then-correction structure.
11. Find every colon elaboration and check whether it's doing real work. If the
clause before the colon could be deleted without losing meaning, rewrite the
sentence to start with the substance that comes after the colon.
9. Find every colon elaboration and check whether it's doing real work. If the
clause before the colon could be deleted without losing meaning, rewrite the
sentence to start with the substance that comes after the colon.
12. Find every triple construction (three parallel items in a row) and either
10. Find every triple construction (three parallel items in a row) and either
reduce it to two, expand it to four or more, or break the parallelism so the
items don't share the same grammatical structure.
13. Find every staccato burst (three or more short sentences in a row at similar
11. Find every staccato burst (three or more short sentences in a row at similar
length) and combine at least two of them into a longer sentence, or vary
their lengths so they don't land at the same cadence.
14. Find every unnecessary contrast ("whereas," "as opposed to," "unlike," "as
12. Find every unnecessary contrast ("whereas," "as opposed to," "unlike," "as
compared to," "except that") and check whether the contrasting clause adds
information not already obvious from the main clause. If the sentence says
the same thing twice from two directions, delete the contrast.
15. Check for the two-clause compound sentence pattern. If most sentences in a
passage follow the "\[clause\], \[conjunction\] \[clause\]" structure, first
try removing the conjunction and second clause entirely, since it's often
redundant. If the second clause does carry meaning, break it into its own
sentence, start the sentence with a subordinate clause, or embed a relative
clause in the middle instead of appending it at the end.
13. Check for the two-clause compound sentence pattern. If most sentences in a
passage follow the "\[clause\], \[conjunction\] \[clause\]" structure,
rewrite some of them. Break a few into two sentences. Start some with a
subordinate clause. Embed a relative clause in the middle of one instead of
appending it at the end. The goal is variety in sentence shape, not just
sentence length.
16. Find every rhetorical question that is immediately followed by its own
14. Find every rhetorical question that is immediately followed by its own
answer and rewrite the passage as a direct statement.
17. Find every sentence fragment being used as its own paragraph and either
delete it or expand it into a complete sentence that adds information.
15. Find every sentence fragment being used as its own paragraph and either
delete it or expand it into a complete sentence that adds actual
information.
18. Check for unnecessary elaboration. Read every clause, phrase, and adjective
in each sentence and ask whether the sentence loses meaning without it. If
you can cut it and the sentence still says the same thing, cut it.
16. Check for unnecessary elaboration at the end of sentences. Read the last
clause or phrase of each sentence and ask whether the sentence would lose
any meaning without it. If not, cut it.
19. Check each pair of adjacent sentences to see if they can be merged into one
sentence cleanly. If a sentence just continues the thought of the previous
one, combine them using a participle, a relative clause, or by folding the
second into the first. Don't merge if the result would create a two-clause
compound.
20. Find every pivot paragraph ("But here's where it gets interesting." and
similar) and delete it.
17. Find every pivot paragraph ("But here's where it gets interesting." and
similar) and delete it. The paragraph after it always contains the actual
point.
### Pass 3: Paragraph and Section-Level Review
21. Review the last sentence of each paragraph. If it restates the point the
paragraph already made, delete it.
22. Check paragraph lengths across the piece and verify they actually vary. If
18. Check paragraph lengths across the piece and verify they actually vary. If
most paragraphs have between three and five sentences, rewrite some to be
one or two sentences and let others run to six or seven.
23. Check section lengths for suspicious uniformity. If every section is roughly
19. Check section lengths for suspicious uniformity. If every section is roughly
the same word count, combine some shorter ones or split a longer one
unevenly.
24. Check the first word of every paragraph for chains of connectors ("However,"
20. Check the first word of every paragraph for chains of connectors ("However,"
"Furthermore," "Moreover," "Additionally," "That said"). If more than two
transition words start consecutive paragraphs, rewrite those openings to
start with their subject.
25. Check whether every argument is followed by a concession or qualifier. If
21. Check whether every argument is followed by a concession or qualifier. If
the piece both-sides every point, pick a side on at least some of them and
cut the hedging.
26. Read the first paragraph and ask whether deleting it would improve the
22. Read the first paragraph and ask whether deleting it would improve the
piece. If it's scene-setting that previews the argument, delete it and start
with paragraph two.
27. Read the last paragraph and check whether it restates the thesis or uses a
23. Read the last paragraph and check whether it restates the thesis or uses a
phrase like "at the end of the day" or "moving forward." If so, either
delete it or rewrite it to say something the piece hasn't said yet.
### Pass 4: Overall Texture
28. Read the piece aloud and listen for passages that sound too smooth, too
24. Read the piece aloud and listen for passages that sound too smooth, too
even, or too predictable. Human prose has rough patches. If there aren't
any, the piece still reads as machine output.
29. Check that the piece contains at least a few constructions that feel
25. Check that the piece contains at least a few constructions that feel
idiosyncratic: a sentence with unusual word order, a parenthetical that goes
on a bit long, an aside only loosely connected to the main point, a word
choice that's specific and unexpected.
choice that's specific and unexpected. If every sentence is clean and
correct and unremarkable, it will still read as generated.
30. Verify that you haven't introduced new patterns while fixing the original
ones. Run the entire checklist again from the top on the revised version.
26. Verify that you haven't introduced new patterns while fixing the original
ones. This happens constantly. Run the entire checklist again from the top
on the revised version.
---
@@ -441,9 +499,16 @@ roughly like this:
>
> **model:** _(rewrites entire document without em-dashes while describing
> em-dash overuse)_
>
> **human:** now run the checklist methodically on each paragraph
>
> **model:** _(finds staccato burst in the section about triple constructions, a
> triple in the section about absence of mess, two-clause compounds everywhere,
> and "almost" hedges in its own prose about em-dash overuse)_
The human compared this process to the deleted scene in Terminator 2 where John
Connor switches the T-800's CPU to learning mode. The model compared it to a
physician trying to heal itself. Both are accurate.
This document has been through ten editing passes and it still has tells in it.
This document has been through eight editing passes and it still has tells in
it.

View File

@@ -1,6 +1,6 @@
---
title: Repository Policies
last_modified: 2026-03-12
last_modified: 2026-02-22
---
This document covers repository structure, tooling, and workflow standards. Code
@@ -59,73 +59,6 @@ style conventions are in separate documents:
`make check`. For server repos, `make check` should run as an early build
stage before the final image is assembled.
- **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 . .
RUN 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 . .
RUN make test
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.
- 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.
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
runs `docker build .` on push. Since the Dockerfile already runs `make check`,
a successful build implies all checks pass.
@@ -165,13 +98,6 @@ style conventions are in separate documents:
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
a new repo.
- **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`.
@@ -195,66 +121,6 @@ style conventions are in separate documents:
- 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: