fix(server): wrap the shared client-name alternation like claudewatcher does (#209) #214

Open
mathias wants to merge 2 commits from wt/client-pattern into main
Owner

Sprint #209, PR-D (small). Risk: medium by label (part of the client-content filter). Routing is still OFF in production.

Found while preparing the infra change that turns routing on: the deployment injects CLAUDE_INGEST_CLIENT_BLOCK (SOPS/ExternalSecret claude-ingest-client-block) as a bare alternation of client names, and claudewatcher compiles it as (?i)\b(<names>)\b (cmd/server/main.go, claudewatcher scrubber). The router introduced in #211 used it raw as its fallback pattern, so a short client name would match inside ordinary words (dif in "different", "modifying") and park clips that have nothing to do with the client — fail-closed, but it would swamp needs-review/ the day routing is enabled.

Change

ingestClientPattern(getenv): INGEST_CLIENT_BLOCK is a complete regexp, used as is; otherwise CLAUDE_INGEST_CLIENT_BLOCK is wrapped \b(...)\b. The router's own case-insensitivity and Unicode folding still apply on top.

Evidence

Tests first, red then green; mutation-checked (unwrapped fallback fails both tests): a short client name does not park "a different approach to modifying prompts", and does park "kickoff for the DIF project" and "Trucker, the client, asked for". go vet, go test -race ./... exit 0, 0 FAIL, lint 0.

Not covered

The folding views (whitespace removed) interact with \b: the collapsed view keeps word boundaries, the whitespace-free view does not, so \bname\b is evaluated mainly against the as-is and collapsed views. Stated in docs/raw-inbox.md limits.

🤖 Generated with Claude Code


Created via git-mcp on behalf of @mathias

Sprint #209, PR-D (small). Risk: medium by label (part of the client-content filter). Routing is still OFF in production. Found while preparing the infra change that turns routing on: the deployment injects `CLAUDE_INGEST_CLIENT_BLOCK` (SOPS/ExternalSecret `claude-ingest-client-block`) as a **bare alternation of client names**, and `claudewatcher` compiles it as `(?i)\b(<names>)\b` (cmd/server/main.go, claudewatcher scrubber). The router introduced in #211 used it **raw** as its fallback pattern, so a short client name would match inside ordinary words (`dif` in "different", "modifying") and park clips that have nothing to do with the client — fail-closed, but it would swamp `needs-review/` the day routing is enabled. ## Change `ingestClientPattern(getenv)`: `INGEST_CLIENT_BLOCK` is a complete regexp, used as is; otherwise `CLAUDE_INGEST_CLIENT_BLOCK` is wrapped `\b(...)\b`. The router's own case-insensitivity and Unicode folding still apply on top. ## Evidence Tests first, red then green; mutation-checked (unwrapped fallback fails both tests): a short client name does not park "a different approach to modifying prompts", and does park "kickoff for the DIF project" and "Trucker, the client, asked for". `go vet`, `go test -race ./...` exit 0, 0 FAIL, lint 0. ## Not covered The folding views (whitespace removed) interact with `\b`: the collapsed view keeps word boundaries, the whitespace-free view does not, so `\bname\b` is evaluated mainly against the as-is and collapsed views. Stated in docs/raw-inbox.md limits. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Created via git-mcp on behalf of @mathias_
mathias added 1 commit 2026-10-20 19:39:42 +00:00
fix(server): wrap the shared client-name alternation like claudewatcher does (#209)
CI / Lint / Test / Vet (pull_request) Successful in 1m6s
CI / Mirror to GitHub (pull_request) Skipped
0582dfdf6b
The deployment injects CLAUDE_INGEST_CLIENT_BLOCK as a bare alternation of client
names; claudewatcher compiles it as (?i)\b(...)\b. The raw/ router used it raw, so
a short client name would match inside ordinary words and park clips that have
nothing to do with the client. INGEST_CLIENT_BLOCK stays a full regexp, used as
is. Mutation-checked.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
mathias added 1 commit 2026-10-20 19:44:25 +00:00
fix(wing,server): client names are matched as whole Unicode words; unsafe lists are a startup error (review of #214)
CI / Lint / Test / Vet (pull_request) Successful in 1m3s
CI / Mirror to GitHub (pull_request) Skipped
80f3fd3c5e
- WrapNames bounds the bare name list with Unicode-aware edges instead of \b
  (Go's \b is ASCII-only, so a name with a non-ASCII edge letter such as Åke,
  Société, Ørsted would never have matched), and validates the list on its own:
  it must compile, must not match the empty string, and cannot break out of its
  group ('x)|.*(' compiles alone only if balanced)
- NewRouter refuses any pattern that matches text naming no client
- docs state the whole-word semantics and what is not matched
Mutation-checked. Open decision for Mathias: whole-word versus substring recall
for a confidentiality filter (plurals, CamelCase and digits glued on do not match).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Author
Owner

Independent review pass 1 (pr-gatekeeper agent, by execution): BLOCK. Resolution at 80f3fd3 (tests first, mutation-checked):

  1. HIGH non-ASCII edge letters never matched. Go's \b is ASCII-only, so a name like Åke, Société, Ørsted, Zoë had no boundary against a space and was never parked (the unwrapped code had matched them by substring: a regression I introduced; claudewatcher's \b(...)\b has the same hole, inherited). Replaced with Unicode-aware edges: (?:^|[^\p{L}\p{N}])(?:names)(?:[^\p{L}\p{N}]|$). Tests with Å/é/Ø names.
  2. MEDIUM the list could change the structure (x)|.*( -> every note parked; acme)|(b). WrapNames validates the list ON ITS OWN first (must compile, must not match "", so it cannot break out of the group). Startup error otherwise.
  3. LOW empty alternatives (a||b, acme|, |acme) parked everything with no signal. Refused at startup; NewRouter also refuses any full pattern that matches "" or "x".
  4. Tests extended: non-ASCII names, empty alternatives, ) breakout, bad patterns, precedence of INGEST_CLIENT_BLOCK. Import grouping fixed.
  5. Equivalence with claudewatcher: same secret, a compatible but not identical match (the router also folds Unicode/whitespace views and uses Unicode word edges; claudewatcher keeps \b). Stated in the docs.

Not changed, needs Mathias's decision: whole-word versus substring recall for a confidentiality filter. As it stands plurals (truckers), CamelCase (AcmeCorp) and digits glued on (acme2) pass; trucker_ecommerce, Trucker's, fullwidth, Cyrillic lookalikes, zero-width splits and t r u c k e r are parked. Wider (substring) recall catches more but would also park a short name inside ordinary words (dif in "different"). I do not know the real names, so I cannot size either error. Also noted: the whitespace-free view makes word edges mean punctuation only, so single-letter fragments split by spaces (co-a b for ab) can park an unrelated note; the upside is that d i f is caught.

Gate: go vet ./..., go test -race ./... exit 0, 0 FAIL, lint 0.


Created via git-mcp on behalf of @mathias

**Independent review pass 1 (pr-gatekeeper agent, by execution): BLOCK. Resolution at 80f3fd3** (tests first, mutation-checked): 1. **HIGH non-ASCII edge letters never matched.** Go's `\b` is ASCII-only, so a name like `Åke`, `Société`, `Ørsted`, `Zoë` had no boundary against a space and was never parked (the unwrapped code had matched them by substring: a regression I introduced; claudewatcher's `\b(...)\b` has the same hole, inherited). Replaced with Unicode-aware edges: `(?:^|[^\p{L}\p{N}])(?:names)(?:[^\p{L}\p{N}]|$)`. Tests with Å/é/Ø names. 4. **MEDIUM the list could change the structure** (`x)|.*(` -> every note parked; `acme)|(b`). `WrapNames` validates the list ON ITS OWN first (must compile, must not match "", so it cannot break out of the group). Startup error otherwise. 5. **LOW empty alternatives (`a||b`, `acme|`, `|acme`) parked everything with no signal.** Refused at startup; `NewRouter` also refuses any full pattern that matches "" or "x". 7. Tests extended: non-ASCII names, empty alternatives, `)` breakout, bad patterns, precedence of `INGEST_CLIENT_BLOCK`. Import grouping fixed. 8. Equivalence with claudewatcher: same secret, a compatible but not identical match (the router also folds Unicode/whitespace views and uses Unicode word edges; claudewatcher keeps `\b`). Stated in the docs. **Not changed, needs Mathias's decision:** whole-word versus substring recall for a *confidentiality* filter. As it stands plurals (`truckers`), CamelCase (`AcmeCorp`) and digits glued on (`acme2`) pass; `trucker_ecommerce`, `Trucker's`, fullwidth, Cyrillic lookalikes, zero-width splits and `t r u c k e r` are parked. Wider (substring) recall catches more but would also park a short name inside ordinary words (`dif` in "different"). I do not know the real names, so I cannot size either error. Also noted: the whitespace-free view makes word edges mean punctuation only, so single-letter fragments split by spaces (`co-a b` for `ab`) can park an unrelated note; the upside is that `d i f` is caught. Gate: `go vet ./...`, `go test -race ./...` exit 0, 0 FAIL, lint 0. --- _Created via git-mcp on behalf of @mathias_
All checks were successful
CI / Lint / Test / Vet (pull_request) Successful in 1m3s
Required
Details
CI / Mirror to GitHub (pull_request) Skipped
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin wt/client-pattern:wt/client-pattern
git checkout wt/client-pattern
Sign in to join this conversation.