Skip to main content
Glama
README.md
# redactpdf

Sharing a PDF usually means trusting that whatever you
blacked out on screen is actually gone. Often it is not:
the text is still sitting in the file, ready to be copied
out. redactpdf takes the opposite approach. It looks for
sensitive text in a local PDF's text layer, writes a copy
in which every matched character has been physically
replaced, and then re-reads its own output to prove that
nothing it was asked to remove can still be extracted.
If the proof fails, you get an error instead of a file.

Four kinds of sensitive text are detected. Social Security
numbers must satisfy the SSA's own validity rules (area
001-899 except 666, group 01-99, serial 0001-9999), so
random digit groups in that shape are not flagged.
Payment-card numbers are digit runs of 13 to 19 digits,
allowing single spaces or dashes between digits, and must
pass the Luhn check. Email addresses and North-American
phone numbers — `(ddd) ddd-dddd`, `ddd-ddd-dddd`,
`ddd.ddd.dddd`, or `ddd ddd dddd`, with an optional `+1`
prefix — round out the built-in categories. On top of
those, `terms` lets you name literal strings of your own,
matched case-insensitively and counted under their own
key. Omit `detect` to run all four categories.

Detection runs per extracted string — the string operands
of `Tj` and `TJ` show operators, in plain or FlateDecode
content streams — so a match that is split across two
string operands is not seen. That is a stated limitation
of the text-layer model this tool uses, not a silent one.

`check_pdf` reports what would be removed without writing
anything. `redact_pdf` writes the redacted copy — the
input file is never modified — replacing each matched
character with `#` while preserving string lengths, then
verifies the result. Documents run to 10 pages free; past
that, both tools ask for a licence. The licence check in
this build is a format check — a token of 16 to 512
characters from `[A-Za-z0-9_-]` plus `.` separators counts
as well-formed, supplied via the `license` argument or
the `BLACKOUT_LICENSE` environment variable — and the
over-limit error distinguishes a missing licence
(`LICENSE_REQUIRED`) from a malformed one
(`LICENSE_INVALID`). Encrypted PDFs, PDFs built on xref
or object streams, and malformed files are reported as
unsupported rather than passed over in silence.

Both tools answer with a one-line summary, a blank line,
and a fenced JSON block carrying `path`, `outputPath`
(null for `check_pdf`), `pages`, `counts`,
`extractableChars` — for `check_pdf`, the characters that
fall inside a detected match in the input; for a
successful `redact_pdf`, 0 by construction — and
`licenseState`.

## CLI

```bash
npx @reneturcios/redactpdf check_pdf '{"path":"/abs/path/report.pdf"}'
npx @reneturcios/redactpdf redact_pdf '{"path":"/abs/path/report.pdf","terms":["falcon"]}'
npx @reneturcios/redactpdf --version
```

`redact_pdf` writes `<input>-redacted.pdf` (the input
path with its `.pdf` suffix replaced) unless
`output_path` says otherwise; the output may not be the
input itself, and an existing output is only replaced
when `overwrite` is true.

Exit codes: `0` on success, `1` on a runtime error (the
tool's error text is printed), `2` on a usage error.

## MCP server

```bash
npx -y @reneturcios/redactpdf mcp
```

MCP client config:

```json
{
  "mcpServers": {
    "redactpdf": {
      "command": "npx",
      "args": ["-y", "@reneturcios/redactpdf", "mcp"]
    }
  }
}
```

### Tools

- `check_pdf` — Report what would be redacted from a
  local PDF — per-category match counts — without writing
  anything.
  - `path` (string, required) — absolute path to a PDF
    on this machine
  - `detect` (string array, optional) — any of `ssn`,
    `card`, `email`, `phone`; omit for all
  - `terms` (string array, optional) — literal strings
    to match, case-insensitive
- `redact_pdf` — Write a redacted copy of a local PDF
  with detected sensitive text permanently removed and
  verified gone. Never modifies the input.
  - `path` (string, required) — absolute path to a PDF
    on this machine
  - `output_path` (string, optional) — defaults to
    `<input>-redacted.pdf`; must not be the input
  - `detect` (string array, optional) — any of `ssn`,
    `card`, `email`, `phone`; omit for all
  - `terms` (string array, optional) — literal strings
    to match, case-insensitive
  - `overwrite` (boolean, optional, default false) —
    allow replacing an existing output
  - `license` (string, optional) — Pro licence token;
    falls back to `BLACKOUT_LICENSE`

## Docker

```bash
docker run -i ghcr.io/cloudcorprecords/redactpdf
docker run --rm -v "$PWD:/w" ghcr.io/cloudcorprecords/redactpdf check_pdf '{"path":"/w/report.pdf"}'
```

## Development

```bash
npm install
npm run build   # tsc -> dist/
npm test        # tsx --test test/*.test.ts (pure local, no network)
npm run dev     # run the CLI from source
```

## License

MIT — Copyright (c) 2026 Rene Turcios