#14: add a Cloudflare Pages _headers file with baseline security headers (closes #14) #37

Merged
clawbot merged 1 commits from issue-14-security-headers into main 2026-08-09 19:08:56 +02:00
Collaborator

Closes #14.

One commit, two files: a new static/_headers and TODO.md.

The one thing a reviewer must not skip

These headers are unverified in production and cannot be verified from this
branch.
Cloudflare Pages silently ignores a malformed _headers file, so the
green build and the browser testing below prove the policy is correct and
prove nothing about whether Pages parses the file. That check needs a live
deploy and belongs after merge.

When it is done, it must be made on strict-transport-security or
content-security-policy. Checking x-content-type-options would be a false
pass: Cloudflare already sends that one whether or not the file took effect. It
must be run against both hostnames, not just the apex — dropping
includeSubDomains rests on www.lora.vegas being served by this same Pages
project, and that was established behaviourally rather than from the Cloudflare
dashboard:

curl -sSI https://lora.vegas/     | grep -iE 'strict-transport|content-security'
curl -sSI https://www.lora.vegas/ | grep -iE 'strict-transport|content-security'

If www turns out not to be covered, the includeSubDomains decision has to be
revisited.

Before state, measured

curl -sSI https://lora.vegas/, run against the live site while writing this:

access-control-allow-origin: *
x-content-type-options: nosniff
referrer-policy: strict-origin-when-cross-origin

No strict-transport-security, no content-security-policy, no
x-frame-options, no permissions-policy. This confirms the correction in the
issue comment rather than the issue body's "no response security headers"
opening: two of the six are already served by Cloudflare's defaults, so those
two lines in the file are restatements and the substance of this change is the
other four.

Also confirmed: https://lora.vegas/ and https://www.lora.vegas/ return
byte-identical bodies, and both hash equal to the public/index.html this
branch builds. Same Pages project, two custom domains, deployment current with
main. That fact decides the HSTS question below.

The headers

/*
  Strict-Transport-Security: max-age=31536000
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin-when-cross-origin
  X-Frame-Options: DENY
  Permissions-Policy: geolocation=(), microphone=(), camera=()
  Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none'

The file carries its reasoning in # comments, which Cloudflare's _headers
format supports.

CSP

Checked against the built page, not copied from a template. public/index.html
contains no script, img, link, iframe, form, video, audio,
object or embed element, no style= attribute and no on*= handler. It has
exactly one inline <style> block, which
themes/loravega/layouts/_default/baseof.html fills with the whole of
themes/loravega/static/css/style.css via readFile.

So default-src 'none' holds, and 'unsafe-inline' in style-src is required
by — and only by — that inlining, which is a deliberate theme design choice. No
script-src allowance, because there are no scripts to allow.

img-src 'self' is kept although the page has no images. Measured both ways in
Chrome: with the allowance the browser's unprompted /favicon.ico request is
made and 404s; without it the request is suppressed entirely. Neither is a
problem today, so this is a forward-looking same-origin allowance, not a fix —
it loosens nothing cross-origin. (My plan comment on the issue claimed the
bare-default-src case logs a violation event; that turned out to be wrong, and
the file comment records what actually happens.)

X-Frame-Options: DENY and frame-ancestors 'none' agree. Nothing in the repo
or the issue suggests an intent to let others embed the channel listings; if
that changes, both move together.

HSTS: no includeSubDomains, no preload

preload is omitted for the reason the issue gives — effectively irreversible,
an owner decision.

includeSubDomains is dropped, and the deciding evidence is that
www.lora.vegas is served by this same Pages project, so this _headers block
sets HSTS on its responses directly. That is the concrete present-day gap
includeSubDomains would otherwise close, and it is closed without it. Beyond
www, no other name under lora.vegas resolves at all (checked mqtt, map,
meshmap, mail, api, dev, mesh, node, admin, test, staging).

So includeSubDomains buys nothing today, while committing every future
subdomain to HTTPS-only in browsers for a year. It is the half-step to
preload, and it shares preload's asymmetry: it cannot be walked back inside
the max-age window without also dropping the apex protection. For a community
mesh site where a map. or mqtt. host on someone's home server is a plausible
next move, that is the owner's call to make deliberately, not mine to make by
copying a baseline. Left as a tracked decision in TODO.md.

access-control-allow-origin: *

Left alone, deliberately. It is Cloudflare's default for static assets and this
site has no credentialed endpoints, no cookies, no API and no private origin
behind it — every byte it serves is already public, so permitting cross-origin
reads grants nothing that a plain fetch of the public URL does not. Overriding
it to a narrower value would be pure noise with a real downside: it would break
any third party legitimately fetching the page. Recorded as a decision rather
than left unexamined, per the issue comment.

Permissions-Policy

The issue's three, unchanged. The site uses no browser features at all, so a
longer list would add unrecognized-feature console warnings without adding
protection.

Verification

Deploy path unaffected. This touches no script, no Makefile target, no
Dockerfile and neither .gitea/workflows/deploy.yml nor script/bootstrap.
It adds one static file to the build output.

The theme's static/ is not shadowed. This is the first root-level
static/ in the repo, and that was the real risk. Hugo unions the project's
static/ over the theme's per path, so a root static/_headers cannot displace
themes/loravega/static/css/style.css. Proved rather than asserted, by building
before and after: public/css/style.css is unchanged at
d5b8acd561ca7792de2c81f62e773d743074840fdd547cc642692c090302366a,
public/index.html unchanged at
ca6ff211353833806c1f36b998ea3b436c573f39c92293d0dcd7b135d852d866, the tree
gains exactly one file, and Hugo's static file count goes 1 to 2.

The file lands where Pages reads it. After make test, public/_headers
exists and is byte-identical to static/_headers.

The CSP does not break the page. Not reasoned about — loaded. I served the
built public/ from a local HTTP server that parses the committed _headers
and emits it as real response headers (real headers matter: frame-ancestors
and X-Frame-Options do not work from a <meta> tag), then drove it with
headless Chrome 137 while collecting securitypolicyviolation events, console
errors and failed requests:

  • CSP violations: zero. All six headers arrived at the browser intact.
  • Styling renders fully. The inline block parses to 17 CSS rules, and the
    computed style is the theme's, not the browser's: body padding 32px 16px,
    max-width 90%, line-height 25.6px, tagline rgb(102, 102, 102), links
    rgb(0, 102, 204) — the --muted and --accent values from style.css.
  • Framing is refused. Embedding the page in an iframe from another origin
    yields "Refused to frame ... because an ancestor violates ... frame-ancestors
    'none'" and an empty frame.
  • All five named outbound links still navigate, clicked from inside the
    CSP-protected document: meshtastic.org, signal.group, discord.gg (redirects to
    discord.com/invite/...), sneak.berlin, git.eeqj.de — every one status 200,
    zero CSP violations on navigation. As expected: no directive in this policy
    governs top-level anchor navigation.
  • The only console noise is the /favicon.ico 404 discussed above, and one
    sgnl:// ERR_ABORTED from signal.group's own protocol-handler attempt after
    navigation, which is that site's behaviour and not ours.

make check passes: clean hugo --minify build, --printPathWarnings lint
build, prettier check. make fmt leaves the tree unchanged. _headers has no
extension and so is outside prettier's **/*.md / **/*.css scope.

script/cibuild succeeds with the check layer genuinely executed, not
CACHED — on the reworked head the layer ran 2.4s and emitted both Hugo builds
and the prettier output, with Static files 2 confirming the new file is in the
image build too.

TODO.md

The Workflow section's rotation was not applied, deliberately: Next Step is
LICENSE (#10), which is neither
this work nor done — it is blocked on the owner's choice of license. Moving it
to Completed Steps would be a false claim. Instead this adds the Completed entry
for this work, updates Status, and adds two Future Steps: the post-merge live
header check on both hostnames, and the deferred HSTS
includeSubDomains/preload decision.

Closes https://git.eeqj.de/sneak/lora.vegas/issues/14. One commit, two files: a new `static/_headers` and `TODO.md`. ## The one thing a reviewer must not skip **These headers are unverified in production and cannot be verified from this branch.** Cloudflare Pages silently ignores a malformed `_headers` file, so the green build and the browser testing below prove the *policy* is correct and prove nothing about whether Pages parses the *file*. That check needs a live deploy and belongs after merge. When it is done, it must be made on `strict-transport-security` or `content-security-policy`. Checking `x-content-type-options` would be a false pass: Cloudflare already sends that one whether or not the file took effect. It must be run against **both** hostnames, not just the apex — dropping `includeSubDomains` rests on `www.lora.vegas` being served by this same Pages project, and that was established behaviourally rather than from the Cloudflare dashboard: ``` curl -sSI https://lora.vegas/ | grep -iE 'strict-transport|content-security' curl -sSI https://www.lora.vegas/ | grep -iE 'strict-transport|content-security' ``` If `www` turns out not to be covered, the `includeSubDomains` decision has to be revisited. ## Before state, measured `curl -sSI https://lora.vegas/`, run against the live site while writing this: ``` access-control-allow-origin: * x-content-type-options: nosniff referrer-policy: strict-origin-when-cross-origin ``` No `strict-transport-security`, no `content-security-policy`, no `x-frame-options`, no `permissions-policy`. This confirms the correction in the issue comment rather than the issue body's "no response security headers" opening: two of the six are already served by Cloudflare's defaults, so those two lines in the file are restatements and the substance of this change is the other four. Also confirmed: `https://lora.vegas/` and `https://www.lora.vegas/` return byte-identical bodies, and both hash equal to the `public/index.html` this branch builds. Same Pages project, two custom domains, deployment current with `main`. That fact decides the HSTS question below. ## The headers ``` /* Strict-Transport-Security: max-age=31536000 X-Content-Type-Options: nosniff Referrer-Policy: strict-origin-when-cross-origin X-Frame-Options: DENY Permissions-Policy: geolocation=(), microphone=(), camera=() Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src 'self'; form-action 'none'; frame-ancestors 'none'; base-uri 'none' ``` The file carries its reasoning in `#` comments, which Cloudflare's `_headers` format supports. ### CSP Checked against the built page, not copied from a template. `public/index.html` contains no `script`, `img`, `link`, `iframe`, `form`, `video`, `audio`, `object` or `embed` element, no `style=` attribute and no `on*=` handler. It has exactly one inline `<style>` block, which `themes/loravega/layouts/_default/baseof.html` fills with the whole of `themes/loravega/static/css/style.css` via `readFile`. So `default-src 'none'` holds, and `'unsafe-inline'` in `style-src` is required by — and only by — that inlining, which is a deliberate theme design choice. No `script-src` allowance, because there are no scripts to allow. `img-src 'self'` is kept although the page has no images. Measured both ways in Chrome: with the allowance the browser's unprompted `/favicon.ico` request is made and 404s; without it the request is suppressed entirely. Neither is a problem today, so this is a forward-looking same-origin allowance, not a fix — it loosens nothing cross-origin. (My plan comment on the issue claimed the bare-`default-src` case logs a violation event; that turned out to be wrong, and the file comment records what actually happens.) `X-Frame-Options: DENY` and `frame-ancestors 'none'` agree. Nothing in the repo or the issue suggests an intent to let others embed the channel listings; if that changes, both move together. ### HSTS: no `includeSubDomains`, no `preload` `preload` is omitted for the reason the issue gives — effectively irreversible, an owner decision. `includeSubDomains` is dropped, and the deciding evidence is that `www.lora.vegas` is served by this same Pages project, so this `_headers` block sets HSTS on its responses **directly**. That is the concrete present-day gap `includeSubDomains` would otherwise close, and it is closed without it. Beyond `www`, no other name under `lora.vegas` resolves at all (checked `mqtt`, `map`, `meshmap`, `mail`, `api`, `dev`, `mesh`, `node`, `admin`, `test`, `staging`). So `includeSubDomains` buys nothing today, while committing every *future* subdomain to HTTPS-only in browsers for a year. It is the half-step to `preload`, and it shares `preload`'s asymmetry: it cannot be walked back inside the max-age window without also dropping the apex protection. For a community mesh site where a `map.` or `mqtt.` host on someone's home server is a plausible next move, that is the owner's call to make deliberately, not mine to make by copying a baseline. Left as a tracked decision in `TODO.md`. ### `access-control-allow-origin: *` Left alone, deliberately. It is Cloudflare's default for static assets and this site has no credentialed endpoints, no cookies, no API and no private origin behind it — every byte it serves is already public, so permitting cross-origin reads grants nothing that a plain fetch of the public URL does not. Overriding it to a narrower value would be pure noise with a real downside: it would break any third party legitimately fetching the page. Recorded as a decision rather than left unexamined, per the issue comment. ### Permissions-Policy The issue's three, unchanged. The site uses no browser features at all, so a longer list would add unrecognized-feature console warnings without adding protection. ## Verification **Deploy path unaffected.** This touches no script, no `Makefile` target, no `Dockerfile` and neither `.gitea/workflows/deploy.yml` nor `script/bootstrap`. It adds one static file to the build output. **The theme's `static/` is not shadowed.** This is the first root-level `static/` in the repo, and that was the real risk. Hugo unions the project's `static/` over the theme's per path, so a root `static/_headers` cannot displace `themes/loravega/static/css/style.css`. Proved rather than asserted, by building before and after: `public/css/style.css` is unchanged at `d5b8acd561ca7792de2c81f62e773d743074840fdd547cc642692c090302366a`, `public/index.html` unchanged at `ca6ff211353833806c1f36b998ea3b436c573f39c92293d0dcd7b135d852d866`, the tree gains exactly one file, and Hugo's static file count goes 1 to 2. **The file lands where Pages reads it.** After `make test`, `public/_headers` exists and is byte-identical to `static/_headers`. **The CSP does not break the page.** Not reasoned about — loaded. I served the built `public/` from a local HTTP server that parses the committed `_headers` and emits it as real response headers (real headers matter: `frame-ancestors` and `X-Frame-Options` do not work from a `<meta>` tag), then drove it with headless Chrome 137 while collecting `securitypolicyviolation` events, console errors and failed requests: - **CSP violations: zero.** All six headers arrived at the browser intact. - **Styling renders fully.** The inline block parses to 17 CSS rules, and the computed style is the theme's, not the browser's: body padding `32px 16px`, max-width `90%`, line-height `25.6px`, tagline `rgb(102, 102, 102)`, links `rgb(0, 102, 204)` — the `--muted` and `--accent` values from `style.css`. - **Framing is refused.** Embedding the page in an iframe from another origin yields "Refused to frame ... because an ancestor violates ... frame-ancestors 'none'" and an empty frame. - **All five named outbound links still navigate**, clicked from inside the CSP-protected document: meshtastic.org, signal.group, discord.gg (redirects to discord.com/invite/...), sneak.berlin, git.eeqj.de — every one status 200, zero CSP violations on navigation. As expected: no directive in this policy governs top-level anchor navigation. - The only console noise is the `/favicon.ico` 404 discussed above, and one `sgnl://` `ERR_ABORTED` from signal.group's own protocol-handler attempt after navigation, which is that site's behaviour and not ours. **`make check`** passes: clean `hugo --minify` build, `--printPathWarnings` lint build, prettier check. `make fmt` leaves the tree unchanged. `_headers` has no extension and so is outside prettier's `**/*.md` / `**/*.css` scope. **`script/cibuild`** succeeds with the check layer genuinely executed, not `CACHED` — on the reworked head the layer ran 2.4s and emitted both Hugo builds and the prettier output, with `Static files 2` confirming the new file is in the image build too. ## TODO.md The Workflow section's rotation was **not** applied, deliberately: Next Step is `LICENSE` (https://git.eeqj.de/sneak/lora.vegas/issues/10), which is neither this work nor done — it is blocked on the owner's choice of license. Moving it to Completed Steps would be a false claim. Instead this adds the Completed entry for this work, updates Status, and adds two Future Steps: the post-merge live header check on both hostnames, and the deferred HSTS `includeSubDomains`/`preload` decision.
clawbot added the needs-review label 2026-08-09 18:51:38 +02:00
clawbot added 1 commit 2026-08-09 18:51:38 +02:00
Add a Cloudflare Pages _headers file with baseline security headers (closes #14)
All checks were successful
check / check (push) Successful in 9s
8d639aa258
Hugo copies static/ verbatim into public/, so static/_headers lands at
the deploy output root, which is where Pages reads it from. This is the
first root-level static/ in the repo; Hugo unions it with the theme's
static/ per path rather than shadowing it, and the built tree confirms
that: public/css/style.css and public/index.html are byte-identical to
the previous build and the static file count goes from 1 to 2.

The live "before" was measured rather than assumed. Cloudflare already
sends X-Content-Type-Options and Referrer-Policy by default, so those
two lines are restatements; the substance is Strict-Transport-Security,
Content-Security-Policy, X-Frame-Options and Permissions-Policy, none of
which the site sends today.

Every value is checked against the built page, which loads nothing: no
script, img, link, iframe, form or media element, no style= and no on*=
attribute. It has exactly one inline <style> block, filled by readFile
in baseof.html. So default-src 'none' with style-src 'unsafe-inline' is
both achievable and tight, and 'unsafe-inline' is required by, and only
by, that deliberate inlining. There is no script-src allowance because
there are no scripts. X-Frame-Options: DENY and frame-ancestors 'none'
agree.

HSTS carries neither preload nor includeSubDomains. www.lora.vegas is
the only other name in DNS and it is served by this same Pages project,
so this file sets HSTS on its responses directly; includeSubDomains
would instead bind every future subdomain for a year, with no way to
walk it back inside the max-age window without also dropping the apex
protection.

Verified in a headless Chrome against a local server that parses the
committed _headers and applies it as real response headers: zero CSP
violations, the inlined stylesheet parses to 17 rules with the computed
body padding, tagline colour and link colour all coming from the theme
CSS, framing from another origin refused by frame-ancestors, and all
five named outbound links still navigating with status 200.

Whether Pages actually parses the file cannot be verified from here.
Pages silently ignores a malformed _headers, so the green build proves
nothing about it; that check belongs after the next deploy and must be
made on Strict-Transport-Security or Content-Security-Policy, since
X-Content-Type-Options would pass either way.
clawbot self-assigned this 2026-08-09 18:51:41 +02:00
Author
Collaborator

Review: FAIL — needs-rework

static/_headers is correct and needs no change. Both blocking defects are in
TODO.md; the fix is an edit plus an amend.

Blocking

1. TODO.md, Completed Steps bullet (last line of the new entry) — sentence
truncated mid-clause.
The bullet ends:

includeSubDomains would instead bind every future subdomain for a year with
no way to walk it back inside the max-age window

No terminal punctuation, and the clause that carries the actual point is gone.
The same sentence in the commit message reads "... inside the max-age window
without also dropping the apex protection." The TODO.md copy lost it. This
is the repo's canonical planning document and it now contains an incomplete
sentence. Acceptable: restore the dropped clause and the full stop.

2. TODO.md, Completed Steps bullet — HTML entities inside a Markdown code
span.
The entry contains &#96;&lt;style&gt;&#96;. CommonMark does not
decode entity references inside code spans, so Gitea renders that literally as
&lt;style&gt; rather than as the tag. The escaping is required for tracker
comments, not for a file in the repo — no other Markdown in this repo uses
entities. Acceptable: &#96;<style>&#96;.

Non-blocking

3. Commit subject is 80 characters. Every other subject on main is 48-67.
An amend is needed for the above anyway; shorten to <= 72.

4. The post-merge verification step names only the apex. www.lora.vegas is
covered by this file only if it is the same Pages project. That was established
behaviorally, not from the Cloudflare dashboard — and since dropping
includeSubDomains rests entirely on that inference, the check that proves it
should be part of the follow-up. Suggest the Future Steps entry and the issue
report also curl https://www.lora.vegas/. If www turns out not to be covered,
the includeSubDomains decision has to be revisited.

Verified independently, passing

  • CSP does not break the page. Built public/index.html has no script,
    img, link, iframe, form, base or media element, no style= and no
    on*=; exactly one inline <style>. The inlined CSS has zero url(, zero
    @import, zero @font-face and exactly 17 rule blocks — independently
    matching the reported "17 CSS rules". All 12 anchors are top-level navigations,
    governed by no directive in this policy. So default-src 'none' +
    style-src 'unsafe-inline' is sufficient and nothing else is needed.
    frame-ancestors 'none' and X-Frame-Options: DENY agree.
  • No theme shadowing. Built main, hashed, built the head, hashed:
    public/css/style.css d5b8acd5... and public/index.html ca6ff211...
    byte-identical across both, static count 1 to 2, public/_headers present
    after make test and byte-identical to static/_headers.
  • "Before" state and the HSTS reasoning. Live curl -sSI on apex and www
    reproduces the PR body exactly: no strict-transport-security,
    content-security-policy, x-frame-options or permissions-policy;
    x-content-type-options and referrer-policy present; access-control-allow-origin: *.
    Both bodies hash to ca6ff211..., the same as the local build. Re-probed DNS:
    of www mqtt map meshmap mail api dev mesh node admin test staging ftp vpn home,
    only www resolves. The reasoning holds subject to finding 4.
  • img-src comment is now truthful — it records measured behaviour and no
    longer claims a violation event. See disclosure below.
  • Header values on the merits. Permissions-Policy is right-sized for a site
    that uses no browser features. Nothing meaningful is missing: COOP/CORP buy
    nothing on a scriptless page, and X-XSS-Protection is correctly absent.
    Correctly did not add upgrade-insecure-requests, which would have been
    pointless here and is adjacent to the one plaintext outbound link
    (http://las.packetflood.net:44920/, a cross-origin top-level navigation and
    therefore unaffected either way).
  • make check green and tree unmodified; script/cibuild green with the check
    layer genuinely executed (layer #11, 2.3s, both Hugo builds and prettier output
    emitted, Static files 2) — not CACHED; CI success on 8d639aa;
    mergeable, merge-base is current main 821a293; single commit; two files
    only, no debris; deploy path untouched (tar picks up public/_headers,
    wrangler pages deploy public consumes it); title ends (closes #14);
    _headers is outside prettier's scope; no prohibited attribution anywhere.

Disclosures

  • No headless browser exists on this host, so I did not re-run the browser
    test.
    I verified statically instead, as above. With zero subresources on the
    page that audit covers the same ground, with one exception: the favicon /
    img-src behaviour is reported as measured and I could not re-measure it.
    Nothing rests on it either way.
  • Whether Cloudflare parses the file remains unverifiable pre-merge. The PR
    states this plainly and names strict-transport-security /
    content-security-policy as the headers to check, calling out
    x-content-type-options as a false pass. Both required statements present.
  • Judgement call: the TODO.md Workflow rotation was deliberately not applied
    (Next Step is #10, blocked on the owner). Consistent with how
    #35 was handled. Not a finding.
## Review: FAIL — `needs-rework` `static/_headers` is correct and needs no change. Both blocking defects are in `TODO.md`; the fix is an edit plus an amend. ### Blocking **1. `TODO.md`, Completed Steps bullet (last line of the new entry) — sentence truncated mid-clause.** The bullet ends: > `includeSubDomains` would instead bind every future subdomain for a year with > no way to walk it back inside the max-age window No terminal punctuation, and the clause that carries the actual point is gone. The same sentence in the commit message reads "... inside the max-age window **without also dropping the apex protection.**" The `TODO.md` copy lost it. This is the repo's canonical planning document and it now contains an incomplete sentence. Acceptable: restore the dropped clause and the full stop. **2. `TODO.md`, Completed Steps bullet — HTML entities inside a Markdown code span.** The entry contains ``&#96;&lt;style&gt;&#96;``. CommonMark does not decode entity references inside code spans, so Gitea renders that literally as `&lt;style&gt;` rather than as the tag. The escaping is required for tracker comments, not for a file in the repo — no other Markdown in this repo uses entities. Acceptable: ``&#96;<style>&#96;``. ### Non-blocking **3. Commit subject is 80 characters.** Every other subject on `main` is 48-67. An amend is needed for the above anyway; shorten to &lt;= 72. **4. The post-merge verification step names only the apex.** `www.lora.vegas` is covered by this file only if it is the same Pages project. That was established behaviorally, not from the Cloudflare dashboard — and since dropping `includeSubDomains` rests entirely on that inference, the check that proves it should be part of the follow-up. Suggest the Future Steps entry and the issue report also curl `https://www.lora.vegas/`. If `www` turns out not to be covered, the `includeSubDomains` decision has to be revisited. ### Verified independently, passing - **CSP does not break the page.** Built `public/index.html` has no `script`, `img`, `link`, `iframe`, `form`, `base` or media element, no `style=` and no `on*=`; exactly one inline `<style>`. The inlined CSS has zero `url(`, zero `@import`, zero `@font-face` and exactly 17 rule blocks — independently matching the reported "17 CSS rules". All 12 anchors are top-level navigations, governed by no directive in this policy. So `default-src 'none'` + `style-src 'unsafe-inline'` is sufficient and nothing else is needed. `frame-ancestors 'none'` and `X-Frame-Options: DENY` agree. - **No theme shadowing.** Built `main`, hashed, built the head, hashed: `public/css/style.css` `d5b8acd5...` and `public/index.html` `ca6ff211...` byte-identical across both, static count 1 to 2, `public/_headers` present after `make test` and byte-identical to `static/_headers`. - **"Before" state and the HSTS reasoning.** Live `curl -sSI` on apex and `www` reproduces the PR body exactly: no `strict-transport-security`, `content-security-policy`, `x-frame-options` or `permissions-policy`; `x-content-type-options` and `referrer-policy` present; `access-control-allow-origin: *`. Both bodies hash to `ca6ff211...`, the same as the local build. Re-probed DNS: of `www mqtt map meshmap mail api dev mesh node admin test staging ftp vpn home`, only `www` resolves. The reasoning holds subject to finding 4. - **`img-src` comment is now truthful** — it records measured behaviour and no longer claims a violation event. See disclosure below. - **Header values on the merits.** `Permissions-Policy` is right-sized for a site that uses no browser features. Nothing meaningful is missing: COOP/CORP buy nothing on a scriptless page, and `X-XSS-Protection` is correctly absent. Correctly did **not** add `upgrade-insecure-requests`, which would have been pointless here and is adjacent to the one plaintext outbound link (`http://las.packetflood.net:44920/`, a cross-origin top-level navigation and therefore unaffected either way). - `make check` green and tree unmodified; `script/cibuild` green with the check layer genuinely executed (layer #11, 2.3s, both Hugo builds and prettier output emitted, `Static files 2`) — not `CACHED`; CI `success` on `8d639aa`; mergeable, merge-base is current `main` `821a293`; single commit; two files only, no debris; deploy path untouched (`tar` picks up `public/_headers`, `wrangler pages deploy public` consumes it); title ends ` (closes #14)`; `_headers` is outside prettier's scope; no prohibited attribution anywhere. ### Disclosures - **No headless browser exists on this host, so I did not re-run the browser test.** I verified statically instead, as above. With zero subresources on the page that audit covers the same ground, with one exception: the favicon / `img-src` behaviour is reported as measured and I could not re-measure it. Nothing rests on it either way. - Whether Cloudflare parses the file remains unverifiable pre-merge. The PR states this plainly and names `strict-transport-security` / `content-security-policy` as the headers to check, calling out `x-content-type-options` as a false pass. Both required statements present. - Judgement call: the `TODO.md` Workflow rotation was deliberately not applied (Next Step is #10, blocked on the owner). Consistent with how https://git.eeqj.de/sneak/lora.vegas/pulls/35 was handled. Not a finding.
clawbot added needs-rework and removed needs-review labels 2026-08-09 18:59:39 +02:00
Author
Collaborator

FAIL accepted. Relabelled needs-rework; dispatching rework, then a fresh
reviewer. static/_headers itself is correct and is not being touched.

Finding 2 is my fault, not the implementer's. I instruct every agent to
escape &lt; and &gt; as HTML entities in Gitea bodies, because the tracker
mangles them otherwise. I never scoped that to tracker text, so it was applied
to a repo file — TODO.md now contains `&amp;lt;style&amp;gt;` inside a Markdown
code span, which CommonMark does not decode, so Gitea renders the entities
literally. Tracker-comment escaping leaked into version control because my
instruction did not say where it stops. Every dispatch from here says the rule
applies to Gitea comment and PR bodies only and never to files in the repo.

Finding 1 stands on its own: the TODO.md sentence is truncated mid-clause and
loses the actual reasoning. The commit message has the complete version
("...without also dropping the apex protection"), so the two disagree — and the
one that survives in the repo is the incomplete one.

Findings 3 and 4 folded into the same rework since an amend is required anyway.
Finding 4 is the substantive one: dropping includeSubDomains rests entirely
on www.lora.vegas being the same Pages project, established behaviourally
rather than from the dashboard, so the post-merge check must cover www and
not just the apex.

Worth recording about the review itself: no headless browser exists on that
host, so it could not re-run the author's browser test and said so rather
than implying it had. It verified statically instead — zero script/img/
link/iframe/form/base elements, no inline handlers, one &lt;style&gt;
block, zero url(/@import/@font-face in the CSS, and independently counted
17 rule blocks matching the author's figure. With zero subresources that covers
the same ground, except the favicon behaviour, which nothing rests on. A
reviewer naming the one thing it could not reproduce is worth more than one
that quietly reruns a weaker check.

Not filed, for @sneak: style-src 'unsafe-inline' could be a 'sha256-...'
source expression instead, which would satisfy the policy's preference against
unsafe-inline more strictly. Cost is real — it couples static/_headers to
style.css bytes, so any CSS edit silently breaks styling until the hash is
updated, and it needs _headers moved out of static/ into a Hugo output
format. #14 explicitly blesses unsafe-inline here given the theme inlines CSS
by design. Say if you want it as a follow-up.

FAIL accepted. Relabelled `needs-rework`; dispatching rework, then a fresh reviewer. `static/_headers` itself is correct and is not being touched. **Finding 2 is my fault, not the implementer's.** I instruct every agent to escape `&lt;` and `&gt;` as HTML entities in Gitea bodies, because the tracker mangles them otherwise. I never scoped that to tracker text, so it was applied to a repo file — `TODO.md` now contains `` `&amp;lt;style&amp;gt;` `` inside a Markdown code span, which CommonMark does not decode, so Gitea renders the entities literally. Tracker-comment escaping leaked into version control because my instruction did not say where it stops. Every dispatch from here says the rule applies to Gitea comment and PR bodies only and never to files in the repo. Finding 1 stands on its own: the `TODO.md` sentence is truncated mid-clause and loses the actual reasoning. The commit message has the complete version ("...without also dropping the apex protection"), so the two disagree — and the one that survives in the repo is the incomplete one. Findings 3 and 4 folded into the same rework since an amend is required anyway. Finding 4 is the substantive one: dropping `includeSubDomains` rests entirely on `www.lora.vegas` being the same Pages project, established behaviourally rather than from the dashboard, so the post-merge check must cover `www` and not just the apex. Worth recording about the review itself: no headless browser exists on that host, so it could not re-run the author's browser test and **said so** rather than implying it had. It verified statically instead — zero `script`/`img`/ `link`/`iframe`/`form`/`base` elements, no inline handlers, one `&lt;style&gt;` block, zero `url(`/`@import`/`@font-face` in the CSS, and independently counted 17 rule blocks matching the author's figure. With zero subresources that covers the same ground, except the favicon behaviour, which nothing rests on. A reviewer naming the one thing it could not reproduce is worth more than one that quietly reruns a weaker check. Not filed, for @sneak: `style-src 'unsafe-inline'` could be a `'sha256-...'` source expression instead, which would satisfy the policy's preference against `unsafe-inline` more strictly. Cost is real — it couples `static/_headers` to `style.css` bytes, so any CSS edit silently breaks styling until the hash is updated, and it needs `_headers` moved out of `static/` into a Hugo output format. #14 explicitly blesses `unsafe-inline` here given the theme inlines CSS by design. Say if you want it as a follow-up.
clawbot force-pushed issue-14-security-headers from 8d639aa258 to 5f998c6e70 2026-08-09 19:02:21 +02:00 Compare
Author
Collaborator

Rework pushed: 8d639aa amended to 5f998c6

Still one commit. static/_headers is untouched —
git diff 8d639aa HEAD -- static/_headers is empty. The entire diff against the
reviewed head is TODO.md, 12 insertions and 6 deletions.

1. Truncated sentence — fixed

The Completed Steps bullet now ends:

> ... and includeSubDomains would instead bind every future subdomain for a
> year with no way to walk it back inside the max-age window without also
> dropping the apex protection.

Restored clause and terminal full stop, so TODO.md now carries the same
reasoning as the commit message instead of half of it.

2. HTML entities in a repo file — fixed

`&amp;lt;style&amp;gt;` in the Completed Steps bullet is now `&lt;style&gt;`
with literal characters. Confirmed it was the only leak: grepped the whole
worktree's Markdown for &amp;lt;, &amp;gt;, &amp;amp;, &amp;quot; and numeric character
references — that line was the sole hit, and there are now zero.

3. Commit subject shortened — done

Was 80 characters. Now 71, within the 48-67 neighbourhood of the rest of main
and under the 72 limit:

Add a Cloudflare Pages _headers file with security headers (closes #14)

Still ends with (closes #14). The body is otherwise unchanged except for the
last paragraph, see below.

4. Post-merge check now covers www — done

The Future Steps entry names both hostnames and records why www is not
optional: dropping includeSubDomains rests entirely on www.lora.vegas being
this same Pages project, which was established from identical response bodies
and not from the Cloudflare dashboard. It now reads, in part:

> After the next deploy, confirm the _headers file actually took effect, on
> both https://lora.vegas/ and https://www.lora.vegas/: curl -sSI against
> each must show strict-transport-security or content-security-policy. ...
> www has to be checked too and not just the apex: dropping
> includeSubDomains rests on www.lora.vegas being served by this same Pages
> project, which was established behaviourally from identical response bodies
> rather than from the Cloudflare dashboard. If www turns out not to be
> covered, the includeSubDomains decision has to be revisited (#14)

Carried through to the two other places that instruct the same check, so they do
not disagree with each other: the commit message's closing paragraph and the PR
body's curl snippet, which now runs against both hostnames.

Verification on 5f998c6

  • make check green: hugo --minify build clean, --printPathWarnings lint
    build clean, Static files 2, prettier All matched files use Prettier code style!.
  • make fmt leaves the tree unmodified — TODO.md reported unchanged, so the
    edits were already conforming under the widened '**/*.md' scope, and
    make fmt-check is consequently clean.
  • script/cibuild green with the check layer genuinely executed, not
    CACHED
    : layer #11 [7/7] RUN echo "check epoch: ..." &amp;&amp; make check ran
    DONE 2.4s and emitted both Hugo builds and the prettier line, with
    Static files 2. RUN script/bootstrap stayed CACHED, as intended.
  • git diff 8d639aa HEAD -- static/_headers empty; git status clean; single
    commit on the branch; merge base still current main
    821a293391.

Force-pushed with --force-with-lease pinned to 8d639aa, so the push would
have refused had the branch moved.

Not re-verified in this pass, and unchanged from the reviewed head since the
file it concerns was not touched: the headless-Chrome CSP run and the live
curl probes. Label left needs-rework and assignment left on clawbot for
the relabel.

## Rework pushed: `8d639aa` amended to `5f998c6` Still one commit. `static/_headers` is untouched — `git diff 8d639aa HEAD -- static/_headers` is empty. The entire diff against the reviewed head is `TODO.md`, 12 insertions and 6 deletions. ### 1. Truncated sentence — fixed The Completed Steps bullet now ends: &gt; ... and `includeSubDomains` would instead bind every future subdomain for a &gt; year with no way to walk it back inside the max-age window **without also &gt; dropping the apex protection.** Restored clause and terminal full stop, so `TODO.md` now carries the same reasoning as the commit message instead of half of it. ### 2. HTML entities in a repo file — fixed `` `&amp;lt;style&amp;gt;` `` in the Completed Steps bullet is now `` `&lt;style&gt;` `` with literal characters. Confirmed it was the only leak: grepped the whole worktree's Markdown for `&amp;lt;`, `&amp;gt;`, `&amp;amp;`, `&amp;quot;` and numeric character references — that line was the sole hit, and there are now zero. ### 3. Commit subject shortened — done Was 80 characters. Now 71, within the 48-67 neighbourhood of the rest of `main` and under the 72 limit: ``` Add a Cloudflare Pages _headers file with security headers (closes #14) ``` Still ends with ` (closes #14)`. The body is otherwise unchanged except for the last paragraph, see below. ### 4. Post-merge check now covers `www` — done The Future Steps entry names both hostnames and records *why* `www` is not optional: dropping `includeSubDomains` rests entirely on `www.lora.vegas` being this same Pages project, which was established from identical response bodies and not from the Cloudflare dashboard. It now reads, in part: &gt; After the next deploy, confirm the `_headers` file actually took effect, on &gt; both `https://lora.vegas/` and `https://www.lora.vegas/`: `curl -sSI` against &gt; each must show `strict-transport-security` or `content-security-policy`. ... &gt; `www` has to be checked too and not just the apex: dropping &gt; `includeSubDomains` rests on `www.lora.vegas` being served by this same Pages &gt; project, which was established behaviourally from identical response bodies &gt; rather than from the Cloudflare dashboard. If `www` turns out not to be &gt; covered, the `includeSubDomains` decision has to be revisited (#14) Carried through to the two other places that instruct the same check, so they do not disagree with each other: the commit message's closing paragraph and the PR body's curl snippet, which now runs against both hostnames. ### Verification on `5f998c6` - `make check` green: `hugo --minify` build clean, `--printPathWarnings` lint build clean, `Static files 2`, prettier `All matched files use Prettier code style!`. - `make fmt` leaves the tree unmodified — `TODO.md` reported `unchanged`, so the edits were already conforming under the widened `'**/*.md'` scope, and `make fmt-check` is consequently clean. - `script/cibuild` green with the check layer **genuinely executed, not `CACHED`**: layer `#11 [7/7] RUN echo "check epoch: ..." &amp;&amp; make check` ran `DONE 2.4s` and emitted both Hugo builds and the prettier line, with `Static files 2`. `RUN script/bootstrap` stayed `CACHED`, as intended. - `git diff 8d639aa HEAD -- static/_headers` empty; `git status` clean; single commit on the branch; merge base still current `main` https://git.eeqj.de/sneak/lora.vegas/commit/821a293391d06b1c9ccfc99a8f40fd72a14dfd33. Force-pushed with `--force-with-lease` pinned to `8d639aa`, so the push would have refused had the branch moved. Not re-verified in this pass, and unchanged from the reviewed head since the file it concerns was not touched: the headless-Chrome CSP run and the live `curl` probes. Label left `needs-rework` and assignment left on `clawbot` for the relabel.
clawbot added needs-review and removed needs-rework labels 2026-08-09 19:04:10 +02:00
Author
Collaborator

Re-review of 5f998c6: PASS

Fresh reviewer, did not author the change and did not write the earlier review.
Scoped as instructed: the prior review passed static/_headers on the merits, so
this pass targets the rework delta and the four findings, not a re-derivation of
the CSP.

The critical check — nothing beyond the reported defects moved

git diff 8d639aa 5f998c6 touches TODO.md only, 12 insertions / 6
deletions. git diff 8d639aa 5f998c6 -- static/_headers is empty, and
--stat over all of static/ is empty. The file the prior review cleared on the
merits is byte-identical at the candidate head, so that analysis carries forward
by construction rather than by assertion.

The four findings, each verified closed

  • (a) Truncated sentence — the Completed Steps bullet now ends
    ...inside the max-age window without also dropping the apex protection.
    with the terminal period, carrying the same reasoning as the commit message.
  • (b) Entities in a repo file — the code span is now `&lt;style&gt;`
    with literal characters. I ran the sweep myself rather than taking the report:
    git grep -nE '&(lt|gt|amp|quot|apos|nbsp|#[0-9]+|#x[0-9A-Fa-f]+);' over all
    five tracked Markdown files (README.md, REPO_POLICIES.md, TODO.md,
    archetypes/default.md, content/_index.md) returns zero hits. Widened to
    the entire tree, the sole hit is themes/loravega/layouts/index.html:3
    Meshtastic &amp; LoRa — a correct entity in an HTML file, pre-existing,
    outside this diff.
  • (c) Commit subject — 71 characters, ends (closes #14).
  • (d) www in the post-merge step — all three locations agree, checked
    independently rather than trusting the report: TODO.md Future Steps names
    both https://lora.vegas/ and https://www.lora.vegas/; the commit message's
    closing paragraph says "against both lora.vegas and www.lora.vegas"; the PR
    body's curl snippet runs both. The superseded apex-only entry was replaced, not
    duplicated — grep finds exactly one instruction line in TODO.md.

Checks

make fmt-check clean and make check green with the tree unmodified after
(Static files 2). script/cibuild green in 8.5s with the check layer
genuinely executed — layer #12 [7/7] RUN echo "check epoch: ..." &amp;&amp; make check ran DONE 4.1s and emitted both Hugo builds and the prettier line;
RUN script/bootstrap stayed CACHED, as the CHECK_EPOCH design intends. CI
success on 5f998c6. public/_headers present after the build and
byte-identical to static/_headers.

Probed rather than assumed: the prettier pass on TODO.md could have been an
ignore-file artifact, so I ran a negative control — appending one over-length
unwrapped line to TODO.md makes prettier --check fail on that file, and
removing it restores the pass. The green is real and '**/*.md' genuinely
covers TODO.md. Worktree restored clean.

Everything else

Single commit; merge-base is current main 821a293 and a dry merge is clean
(mergeable: true); two files vs main, no scope creep and no debris;
git status clean at the head; nothing stranded — the remote branch is exactly
5f998c6 and the PR is self-contained, needing no unpushed or uncommitted work.
No Claude or Anthropic reference anywhere in tree, diff, commit message,
trailers, PR body or comments — the only matches are the pre-existing
.claude/ path entries in .gitignore / .dockerignore / TODO.md, all
present on main before this branch and none of them attribution prose.

The PR body still states plainly that Cloudflare Pages parsing is unverifiable
pre-merge, names strict-transport-security / content-security-policy as the
headers to check, and calls out x-content-type-options as a false pass. It
does not overstate.

Disclosures and anomalies

  • Committer identity changed on the amend. 5f998c6 is
    Author: clawbot &lt;clawbot@eeqj.de&gt; but
    Commit: sneak &lt;sneak@sneak.berlin&gt;, where 8d639aa had clawbot for
    both. An artifact of the rework clone's git config, so the amend is recorded
    as committed by sneak. No policy rule in REPO_POLICIES.md covers commit
    identity, the content is unaffected, and I am not filing it as a defect —
    noting it so it is not discovered later as a surprise.
  • I did not re-run the headless-Chrome CSP test or the live curl probes. No
    headless browser on this host, and both concern static/_headers, which is
    unchanged from the head that was already passed on the merits. Nothing in this
    delta could have invalidated them.
  • Cosmetic, not filed: the PR title says "baseline security headers" while the
    commit subject says "security headers". Both end (closes #14).

Labels and assignment left to the caller.

## Re-review of `5f998c6`: PASS Fresh reviewer, did not author the change and did not write the earlier review. Scoped as instructed: the prior review passed `static/_headers` on the merits, so this pass targets the rework delta and the four findings, not a re-derivation of the CSP. ### The critical check — nothing beyond the reported defects moved `git diff 8d639aa 5f998c6` touches **`TODO.md` only**, 12 insertions / 6 deletions. `git diff 8d639aa 5f998c6 -- static/_headers` is **empty**, and `--stat` over all of `static/` is empty. The file the prior review cleared on the merits is byte-identical at the candidate head, so that analysis carries forward by construction rather than by assertion. ### The four findings, each verified closed - **(a) Truncated sentence** — the Completed Steps bullet now ends `` ...inside the max-age window without also dropping the apex protection. `` with the terminal period, carrying the same reasoning as the commit message. - **(b) Entities in a repo file** — the code span is now `` `&lt;style&gt;` `` with literal characters. I ran the sweep myself rather than taking the report: `git grep -nE '&(lt|gt|amp|quot|apos|nbsp|#[0-9]+|#x[0-9A-Fa-f]+);'` over all five tracked Markdown files (`README.md`, `REPO_POLICIES.md`, `TODO.md`, `archetypes/default.md`, `content/_index.md`) returns **zero hits**. Widened to the entire tree, the sole hit is `themes/loravega/layouts/index.html:3` `` Meshtastic &amp; LoRa `` — a correct entity in an HTML file, pre-existing, outside this diff. - **(c) Commit subject** — 71 characters, ends ` (closes #14)`. - **(d) `www` in the post-merge step** — all **three** locations agree, checked independently rather than trusting the report: `TODO.md` Future Steps names both `https://lora.vegas/` and `https://www.lora.vegas/`; the commit message's closing paragraph says "against both lora.vegas and www.lora.vegas"; the PR body's curl snippet runs both. The superseded apex-only entry was replaced, not duplicated — `grep` finds exactly one instruction line in `TODO.md`. ### Checks `make fmt-check` clean and `make check` green with the tree unmodified after (`Static files 2`). `script/cibuild` green in 8.5s with the check layer **genuinely executed** — layer `#12 [7/7] RUN echo "check epoch: ..." &amp;&amp; make check` ran `DONE 4.1s` and emitted both Hugo builds and the prettier line; `RUN script/bootstrap` stayed `CACHED`, as the `CHECK_EPOCH` design intends. CI `success` on `5f998c6`. `public/_headers` present after the build and byte-identical to `static/_headers`. **Probed rather than assumed:** the prettier pass on `TODO.md` could have been an ignore-file artifact, so I ran a negative control — appending one over-length unwrapped line to `TODO.md` makes `prettier --check` fail on that file, and removing it restores the pass. The green is real and `'**/*.md'` genuinely covers `TODO.md`. Worktree restored clean. ### Everything else Single commit; merge-base is current `main` `821a293` and a dry merge is clean (`mergeable: true`); two files vs `main`, no scope creep and no debris; `git status` clean at the head; nothing stranded — the remote branch is exactly `5f998c6` and the PR is self-contained, needing no unpushed or uncommitted work. No Claude or Anthropic reference anywhere in tree, diff, commit message, trailers, PR body or comments — the only matches are the pre-existing `.claude/` path entries in `.gitignore` / `.dockerignore` / `TODO.md`, all present on `main` before this branch and none of them attribution prose. The PR body still states plainly that Cloudflare Pages parsing is unverifiable pre-merge, names `strict-transport-security` / `content-security-policy` as the headers to check, and calls out `x-content-type-options` as a false pass. It does not overstate. ### Disclosures and anomalies - **Committer identity changed on the amend.** `5f998c6` is `Author: clawbot &lt;clawbot@eeqj.de&gt;` but `Commit: sneak &lt;sneak@sneak.berlin&gt;`, where `8d639aa` had `clawbot` for both. An artifact of the rework clone's git config, so the amend is recorded as committed by `sneak`. No policy rule in `REPO_POLICIES.md` covers commit identity, the content is unaffected, and I am not filing it as a defect — noting it so it is not discovered later as a surprise. - **I did not re-run the headless-Chrome CSP test or the live `curl` probes.** No headless browser on this host, and both concern `static/_headers`, which is unchanged from the head that was already passed on the merits. Nothing in this delta could have invalidated them. - Cosmetic, not filed: the PR title says "baseline security headers" while the commit subject says "security headers". Both end ` (closes #14)`. Labels and assignment left to the caller.
clawbot added merge-ready and removed needs-review labels 2026-08-09 19:08:30 +02:00
Author
Collaborator

PASS accepted from a fresh reviewer independent of both the author and the
first reviewer. merge-ready, merging directly — main is unprotected.

The check that made this cheap: git diff 8d639aa 5f998c6 touches TODO.md
only, and the diff over static/_headers is empty. The file the first review
cleared on the merits is byte-identical at the candidate head, so that analysis
carries forward by construction rather than needing to be redone.

Two things worth keeping from this review:

It ran a negative control on the formatting check. A clean
prettier --check on TODO.md could have been an ignore-file artifact rather
than a real pass, so it appended an over-length unwrapped line, confirmed the
check then fails, and removed it. '**/*.md' genuinely covers the file. That
is the same instinct as the planted-defect control on #13 — proving the harness
can fail before trusting it to pass.

It re-ran the entity sweep itself rather than accepting the author's report,
across all five tracked Markdown files, and correctly identified the one
tree-wide hit as legitimate: themes/loravega/layouts/index.html uses
&amp;amp; in HTML, which is correct there and outside the diff.

Anomaly disclosed, not blocking: 5f998c6 records Author: clawbot but
Commit: sneak, where 8d639aa had clawbot for both. A git-config artifact
of the fresh clone the rework used (the branch was already checked out in
another worktree). No policy covers commit identity and the content is
unaffected, so it is not worth a force-push to fix — noting it so the
provenance is not confusing later.

Cosmetic, not reworked: PR title says "baseline security headers", commit
subject says "security headers".

#14 stays open after this merge. Its definition of done has one step left
that cannot be done from a branch: confirming Cloudflare Pages actually parses
the file. Pages silently ignores a malformed _headers, so nothing verified so
far speaks to it. Running that check now against both hostnames and reporting
on the issue.

PASS accepted from a fresh reviewer independent of both the author and the first reviewer. `merge-ready`, merging directly — `main` is unprotected. The check that made this cheap: `git diff 8d639aa 5f998c6` touches `TODO.md` only, and the diff over `static/_headers` is empty. The file the first review cleared on the merits is byte-identical at the candidate head, so that analysis carries forward by construction rather than needing to be redone. Two things worth keeping from this review: **It ran a negative control on the formatting check.** A clean `prettier --check` on `TODO.md` could have been an ignore-file artifact rather than a real pass, so it appended an over-length unwrapped line, confirmed the check then fails, and removed it. `'**/*.md'` genuinely covers the file. That is the same instinct as the planted-defect control on #13 — proving the harness can fail before trusting it to pass. **It re-ran the entity sweep itself** rather than accepting the author's report, across all five tracked Markdown files, and correctly identified the one tree-wide hit as legitimate: `themes/loravega/layouts/index.html` uses `&amp;amp;` in HTML, which is correct there and outside the diff. Anomaly disclosed, not blocking: `5f998c6` records `Author: clawbot` but `Commit: sneak`, where `8d639aa` had `clawbot` for both. A git-config artifact of the fresh clone the rework used (the branch was already checked out in another worktree). No policy covers commit identity and the content is unaffected, so it is not worth a force-push to fix — noting it so the provenance is not confusing later. Cosmetic, not reworked: PR title says "baseline security headers", commit subject says "security headers". **#14 stays open after this merge.** Its definition of done has one step left that cannot be done from a branch: confirming Cloudflare Pages actually parses the file. Pages silently ignores a malformed `_headers`, so nothing verified so far speaks to it. Running that check now against both hostnames and reporting on the issue.
clawbot merged commit 7d7bec526c into main 2026-08-09 19:08:56 +02:00
Sign in to join this conversation.