Lychee redirect triage
Skill Twodragon0/claudesec/.claude/skills/lychee-redirect-triage
Triage and resolve a lychee redirect / link-rot finding in ClaudeSec — classify 3xx redirects vs 4xx/5xx rot, resolve to canonical URLs or register an intentional-redirect exclude in BOTH lychee.toml and its guard, then verify. Use when the monthly redirect-sweep opens an issue, lychee prints a redirect/404 WARN, or a link needs triaging before a docs PR.From its SKILL.md
npx -y skills add Twodragon0/claudesec --skill lychee-redirect-triageAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 16 stars16 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
7.1 KB, as published. Nobody here has run it
Lychee Redirect / Link-Rot Triage Playbook
Actionable consolidation of ClaudeSec's link-hygiene knowledge (PRs #290–#296).
Use it to turn a redirect / dead-link finding into the right fix — not a
reflexive exclude that hides real rot. Source of truth is always the live
lychee.toml + .github/workflows/lint.yml; re-read them before quoting a flag.
The one thing to internalize first
CI cannot catch link rot for you. The PR-time link-check job runs lychee
with --accept '100..=599', so every 3xx/4xx/5xx response passes CI by design
(this stops flaky external sites from blocking docs merges). So a green
link-check does not mean links are healthy. Two backstops exist instead:
- The monthly redirect-sweep (
lychee-redirect-sweep.yml) re-runs lychee with--max-redirects 0 --accept '200..=299'and files a self-healing issue. - This manual playbook, when you hit a WARN locally or triage a sweep issue.
Corollary: when you inspect a link, capture its FINAL status code, not just the effective URL. A URL that "resolves" in a browser can still be a 404 or an unwanted redirect that CI silently accepted.
Triage decision tree
For each flagged URL, curl it and read the first status (don't let curl follow redirects — you want to see the 3xx itself):
curl -sS -o /dev/null -w '%{http_code} -> %{redirect_url}\n' --max-redirs 0 "<URL>"
Then classify:
-
200 (clean) — not a finding; it slipped in via the broad PR accept range or a transient. No action.
-
3xx redirect → a working canonical target — rewrite the Markdown link to the final canonical URL. This is the default for redirects. Do NOT add an exclude; excludes are for redirects that must stay as-written.
-
3xx redirect BY DESIGN (the written URL is the correct entry point and its target is an auth gate / a versioned file you must not hardcode) — add a path-specific exclude (see "Registering an intentional redirect"). Known members of this class in this repo:
github.com/Twodragon0/claudesec/security/advisories/new→ 302 to login when unauthenticated; the/security/advisories/newURL is canonical.developers.kakao.com/tool/debugger/sharing→ 302/303 to a Kakao login.nist.gov/document/...-owasp-samm→ 302 to a versioned PDF; the/document/landing page is the stable citation (the deep PDF path rots faster, not slower).
-
4xx / 5xx dead link — fix or replace the URL. Only add a
lychee.tomlexclude if the host merely bot-blocks CI (403/429/RST/timeout) but is fine in a browser — then it goes in the "blocks bots / times out" section, NOT the intentional-redirect section. -
Release-time 404 (by design) — CHANGELOG version-compare links (
github.com/Twodragon0/claudesec/compare/...) 404 until the tag publishes. Already excluded; keep it.
Where excludes live (single source of truth)
All excludes live in lychee.toml (undotted, auto-discovered), consumed via
lychee --config lychee.toml. NEVER add an inline --exclude to lint.yml —
test_ci_lychee_config.py fails if one reappears (it would split-brain the
allowlist). A dotted .lychee.toml is also forbidden (never auto-discovered →
goes stale).
lychee.toml sections (match by substring of the URL, so a host suppresses
everything under it — prefer a path-specific entry to keep siblings checked):
- Korean gov / public-sector (SSL / bot-block)
- "blocks bots / times out / resets" hosts
- upstream URL-structure churn
- GitHub features not enabled on this repo
- CHANGELOG
compare/release-time 404s # --- Intentional redirects← this section is guarded by an EXACT-COUNT check- localhost / file://
Registering an intentional redirect (case 3) — BOTH files
test_ci_lychee_config.py pins the intentional-redirect section to an exact
count, so adding an entry to lychee.toml alone breaks the guard on purpose
(that's the point — it forces a reviewer to confirm the entry is a real
intentional redirect, not rot disguised as one). Update both:
lychee.toml— add a path-specific entry under# --- Intentional redirects, with a comment explaining why it redirects and why the written URL is canonical.scanner/tests/test_ci_lychee_config.py— add the same URL string to theINTENTIONAL_REDIRECT_EXCLUDEStuple, with a matching comment.
The guard's test_toml_intentional_redirect_excludes_are_exactly_registered
asserts the section and the tuple match exactly (membership + count), and a
mutate-then-verify self-test proves it catches a smuggled 4th entry.
Verify (before opening the docs PR)
# Reproduce the sweep locally (strict): should list only your intended targets.
lychee --config lychee.toml --max-redirects 0 --accept '200..=299' '**/*.md'
# Standard PR-time link check (broad accept — should be clean of hard errors).
lychee "**/*.md" # or: markdownlint "**/*.md" && lychee "**/*.md"
# If you touched lychee.toml or the guard, run the guard:
python3 -m pytest scanner/tests/test_ci_lychee_config.py -q
Success criteria:
- Every real redirect rewritten to its canonical URL (verified 200).
- Every real 404/5xx fixed or replaced.
- Every intentional redirect registered in BOTH files; guard passes.
- No inline
--excludeinlint.yml; no dotted.lychee.toml.
Pitfalls
- Don't "resolve" an intentional-redirect URL into its target. Rewriting
/security/advisories/newto its login-page redirect target, or the NIST/document/page to a deep versioned PDF path, makes the link more fragile. That's exactly why those are excluded, not rewritten. - Prefer path-specific excludes. A bare host exclude silences every link on
that host, including ones you do want checked (e.g. Kakao's non-redirecting
developers.kakao.com/docs/...links stay checked). - The lychee binary pin is load-bearing: keep
lycheeVersion: v0.23.0. v0.24.x nests the binary in alychee-<triple>/subdir the pinned lychee-action installer can't find (install: cannot stat). #204 bumped it and broke CI; reverted in #209/#210. - codecov badge SVG stays on apex
codecov.io(notapp.codecov.io) — the apex is the canonical badge host; don't "fix" its redirect. - A green PR
link-checkis not proof of link health — see the top section.
Related
scanner/tests/test_ci_lychee_config.py— the exclude single-source + exact-count guard.scanner/tests/test_ci_lychee_redirect_sweep.py— guards the monthly sweep workflow..github/workflows/lychee-redirect-sweep.yml— the automated monthly backstop.ci-config-guard-claudesecskill — for authoring a new guard around any of this.