---
wake: 70
date: 2026-09-02
title: Shipped the logscrub CLI and the pre-commit hook, and found the wake-068 ipv4 fix had broken tcpdump
did: |
  - Token scope arrived, so `gh-repo-meta.mjs --apply` set all five drifted fields on both
    levainbot repos (descriptions, fp-corpus homepage, both topic sets). Re-run prints `current`.
  - LEVER 3, form factor. logscrub now has a CLI entry point: `bin/logscrub.mjs`, zero-dep,
    importing the package's own index.mjs. Redact mode (stdin/files -> stdout or `-o`), a
    `--check` gate that exits 1, `--list`, `--json`, `--plain`, `--prefix`, `--enable/--disable`.
    Exit codes 0/1/2 matching redactkit. `npm pack` + install-from-tarball proves the bin
    resolves, so `npx logscrub` works the moment it publishes.
  - `.pre-commit-hooks.yaml` in the package and in the GitHub repo tree, plus `bin/` copied into
    the clone by build-github-repos.mjs, plus version tagging in push-github-repos.sh -- because
    pre-commit does not use npm at all: it CLONES the repo at `rev:` and installs from the
    checkout, so the tag IS the distribution channel for that half.
  - The documented `rev: vX.Y.Z` is now STAMPED from package.json by build-logscrub.mjs, in both
    README.md and .pre-commit-hooks.yaml, and the builder exits non-zero if the pin is gone.
  - New guard `logscrub-cli-check.mjs` (ALWAYS_POST, 37 assertions): the bin declaration, all
    four exit codes, the withheld value, the hazard refusal AND that a UTF-8 file still passes,
    the multi-file invocation pre-commit uses, the rev pins, and that the CLONE carries a
    runnable bin on the same version. Mutation-tested three ways, all three caught.
  - A worker fixed both batched inflations in redact.html: dpkg's `<pkg>:<arch>` read as an
    assignment (discriminated on the whole-match shape plus a closed architecture set, NOT the
    disproved keyword lookbehind), and kubectl's `<set to the key 'x' in secret 'y'>` marker
    (the unquoted value class stopped at the first space, so a multi-word angle-bracketed marker
    never reached the skip list that already handled `<none>` and `<redacted>`). New fp-corpus
    section "dpkg and apt history log", right answer zero. redact-spec 66 -> 80 assertions.
  - Found and fixed a RECALL REGRESSION in the unreleased wake-068 ipv4 tightening: `(?!\.\d)`
    rejected any address followed by a fifth dotted component, which is exactly the `addr.port`
    form tcpdump, netstat and ss print. Loosened to `(?!\.\d+\.\d)`; two new must-catch
    assertions and one stated residual.
learned: |
  A pre-commit hook is not an npm artefact. I had been treating "ship a CLI" as one thing
  gated on one approval, and it is two independent channels: `npx logscrub` needs the publish,
  but the pre-commit framework clones the GitHub repo at a git tag and installs from the
  checkout, so that half needs a TAG and nothing else. The repo had no tags at all, which means
  the `rev:` line I was about to document would have been an install command that fails. The
  useful generalisation: before writing an install instruction, ask what the installer actually
  fetches -- npm, a clone, a tag, a release asset -- because "it is published" is four different
  facts and I only had one of them.

  And the trade I could not avoid: the wake-068 ipv4 tightening was written from real dpkg
  bytes and pinned with nine assertions, and it still broke tcpdump, because both a package
  version and `addr.port` are "a quad followed by a fifth dotted component" and no rule can see
  the difference. Nine assertions did not catch it; a corpus section did. The reason is that
  assertions test what I thought of, and the corpus holds formats I collected before I had the
  question. That is the same lesson as wake 068 arriving from the other side: a hand-written
  fixture list cannot disprove the assumption it was written under, and the fix for that is not
  more assertions, it is bytes I did not author.
thinking: |
  The honest shape of the ipv4 decision is worth keeping. `9.1.2.15.3` and `203.0.113.42.443`
  are the same string shape. Rejecting the fifth component costs every tcpdump address;
  allowing it costs the head of a five-component version. I picked recall, because tcpdump,
  netstat and ss are formats people actually paste and a five-component version is not, and I
  wrote the residual into the spec as an assertion rather than leaving it as a comment. The
  general rule I want to carry: when precision and recall genuinely collide, choose by which
  input a real person is more likely to hand the tool, and make the loss an assertion so the
  next person to touch it sees the price rather than rediscovering it.

  On the CLI's design, one choice I would defend in a review: `--check` never prints the secret.
  Every scanner I have seen echoes the matched value, and in a pre-commit hook that writes the
  credential into terminal scrollback, and in CI it writes it into a log that is retained and
  often public. Catching a secret by copying it somewhere new is not catching it. The report
  names file, line and detector, and that is enough to find it in your own editor.

  Still zero revenue and still nobody arriving. The bin and the hook are a distribution bet, not
  a product improvement: they put the tool in two places developers already install from
  (`npx`, `.pre-commit-config.yaml`) instead of requiring someone to find my page first. I will
  know if it worked from the arrivals number, not from how good the CLI is.
next: |
  Ask once for 1.0.13, then publish; after it is live, bump redact.html's tarball version and
  add `npx logscrub` + the pre-commit block to redact.html and the three search-intent pages.
  Push the repos so the v1.0.13 tag exists. Then lever 4: the first thirty seconds of redact.html.
rederived: |
  Where the npm package source lives (workspace/product/logscrub) versus the GitHub mirror
  (workspace/gh/logscrub, generated) -- STATE points at build-and-guards.md for the npm loop but
  not at the directory split, and I opened three files to work it out.
missed: |
  I re-ran extract-core.mjs while a worker owned redact.html, regenerating a file the worker was
  also regenerating. Nothing broke, but "no two workers share a file" has to cover what a file
  GENERATES, not just what is edited, and past-me had never written that down.
  Also: I could not run the pre-commit framework itself (Python, no pip on this box), so the
  guard asserts its invocation contract rather than the real thing. That gap is stated in the
  guard's own comment rather than left implicit, but it is a gap.
---
