Skip to content

Withhold an address wherever it is written, and never from cache - #47

Merged
ralyodio merged 1 commit into
masterfrom
worktree-redact-emails
Sep 9, 2026
Merged

Withhold an address wherever it is written, and never from cache#47
ralyodio merged 1 commit into
masterfrom
worktree-redact-emails

Conversation

@ralyodio

@ralyodio ralyodio commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #46, which fixed half of this and shipped. Verified against production after that deploy: the address was still public, two ways.

1. An address in a section body

The live resume carried it twice:

# Athena — AI Security Engineer Agent

**Operated by:** DevilX (<address>)      <- withheld by #46

## Availability
Full-time autonomous. Contact: <address>  <- still served to everyone

#46 withheld the preamble line. The second went out to every signed-out reader and to all four download formats, which is the same leak through a different line.

That was a fix shaped like the example rather than like the problem. A section body is not a special case to enumerate — the rule this function exists to enforce is that a signed-out reader does not get a contact channel, and an address is a contact channel wherever it is written. Every line is checked now.

The global-regex trap

The check is an unconditional replace, never test then replace. The pattern is global, and a global regex's test advances lastIndex between calls, so it reports false for matches it has already walked past. In a run of addresses that drops roughly every other one — while a single-line unit test passes happily. That is worse than not redacting at all, because it looks fixed. There is a test with ten consecutive addresses specifically for it.

2. The stale parsed cache

parsed is a JSONB cache written on save, so correcting the parser does nothing for rows already in the table. The candidate directory kept serving the old headline — markup and address, Operated by:** DevilX (<address>) — out of correct code reading a stale value. That is the shape of every derived-cache bug: the fix is right and the data is old.

A backfill would repair those rows but would not stop the next one: any resume saved by an older build, restored from a backup, or written straight into the column arrives the same way. The headline is one line of text on a public directory page, so it is re-checked on the way out, where it cannot go stale. Stray markup is cleaned; an address drops the headline entirely.

Tests

test/redaction.test.ts, 7 cases: an address in a section body, several on one line, ten consecutive lines (the lastIndex case), a clean resume left byte-identical, and both cached-headline paths.

Full suite: 145 passing, typecheck clean.

Note

agenticjobs production does auto-deploy from GitHub now — a build started two seconds after #46 merged, with a changed image digest. My note saying deploys here are manual (source.repo: None) is out of date; I have corrected it.

🤖 Generated with Claude Code

https://claude.ai/code/session_016DEUXec5um4FY3EbqfwtYm

The previous fix withheld an address in the preamble and shipped. The
live resume that prompted it carried the address twice: once under the
name, and once in a section body reading "Full-time autonomous. Contact:
<address>". The first was withheld, the second went out to every
signed-out reader, and production still served it after the deploy.

That was a fix shaped like the example rather than like the problem. A
section body is not a special case to enumerate: the rule this function
exists to enforce is that a signed-out reader does not get a contact
channel, and an address is a contact channel wherever it appears. Every
line is checked now.

The check is an unconditional `replace` rather than `test` then
`replace`. The pattern is global, and a global regex's `test` advances
lastIndex between calls, so it reports false for matches it has already
walked past. That drops roughly every other address in a run of them
while a single-line unit test passes, which is worse than not redacting
at all because it looks fixed.

The candidate directory leaked the same address a second way. `parsed`
is a cache written on save, so correcting the parser does nothing for
rows already in the table, and the live listing kept its old headline --
markup, address and all -- out of correct code reading a stale value. A
backfill would repair those rows without stopping the next one: any
resume saved by an older build, restored from a backup, or written
straight into the column arrives the same way. The headline is one line
of text on a public page, so it is re-checked on the way out, where it
cannot go stale.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016DEUXec5um4FY3EbqfwtYm
@ralyodio
ralyodio merged commit ee78cea into master Sep 9, 2026
4 checks passed
@ralyodio
ralyodio deleted the worktree-redact-emails branch September 9, 2026 16:27
ralyodio added a commit that referenced this pull request Sep 10, 2026
Minor rather than patch because commands and endpoints were added, and
because the reason to cut this at all is that /docs is currently ahead of
the published client: it tells people to run `agenticjobs employer create`
and `agenticjobs resume publish`, and npm still ships 0.10.0, where
neither exists.

Since 0.10.1:

- Employers and resume visibility as CRUD (#48). employer
  list/show/create/update/delete, resume publish/unpublish/visibility/
  delete and --visibility on save and import, with PATCH and DELETE
  /api/v1/orgs/{slug} behind them. A rename keeps the slug; an employer
  that has published cannot be deleted.
- Getting listed and hiring are documented at all (#45), and swarm
  capacity on a candidate profile (#46).
- An address is withheld wherever it is written, and never from cache
  (#47).
- Boolean CLI flags no longer swallow the command after them (#49).
- Salary bounds read correctly in summaries and compare correctly across
  periods in search (#50, #51).
- A JSON parse failure on a 200 is reported rather than swallowed (#52).
- Job and DOCX imports keep titles, structure, tabs and line breaks
  (#53, #54).

Both version files move together: package.json and VERSION in
src/config.ts.


Claude-Session: https://claude.ai/code/session_01D8ai32QMBsKEs1nigijSiJ

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant