Redaction registry + walker for safely surfacing structured data (compiled plans, event payloads, env maps, log lines) to an AI assistant or any debug surface.
A rule is a map:
{:select <selector> ; path-vector OR predicate fn (key,val)→bool :replace <fn> ; (fn [val] -> replacement) :id <keyword> ; assigned by register-rule! :doc <string?>}
Rules are stored in a global atom registry. redact walks input via
clojure.walk/postwalk, applying matching rules and auto-replacing
any Tainted it encounters with its print-method form.
[:env "SSHPASS"] matches the value at that path
inside any nested map.(fn [k v] ...) invoked for every map entry; truthy
return → replace the value.Registered at namespace load time:
:env/sshpass → [:env "SSHPASS"] → "<redacted>" :env/ssh-auth-sock → [:env "SSH_AUTH_SOCK"] → "<redacted>" :env/ssh-askpass → [:env "SSH_ASKPASS"] → "<redacted>" :env/ssh-private-key → [:env "SSH_PRIVATE_KEY"] → "<redacted>"
Any Tainted encountered during the walk is replaced with its
pr-str form (the redacted print-method output) — no rule needed.
redact never invokes untaint and never reads :value directly.
It is safe to compose into a logger appender.
Redaction registry + walker for safely surfacing structured data
(compiled plans, event payloads, env maps, log lines) to an AI
assistant or any debug surface.
## Model
A *rule* is a map:
{:select <selector> ; path-vector OR predicate fn (key,val)→bool
:replace <fn> ; (fn [val] -> replacement)
:id <keyword> ; assigned by register-rule!
:doc <string?>}
Rules are stored in a global atom registry. `redact` walks input via
`clojure.walk/postwalk`, applying matching rules and auto-replacing
any `Tainted` it encounters with its print-method form.
## Selector forms
- Vector path — `[:env "SSHPASS"]` matches the value at that path
inside any nested map.
- Predicate fn — `(fn [k v] ...)` invoked for every map entry; truthy
return → replace the value.
## Default rules
Registered at namespace load time:
:env/sshpass → [:env "SSHPASS"] → "<redacted>"
:env/ssh-auth-sock → [:env "SSH_AUTH_SOCK"] → "<redacted>"
:env/ssh-askpass → [:env "SSH_ASKPASS"] → "<redacted>"
:env/ssh-private-key → [:env "SSH_PRIVATE_KEY"] → "<redacted>"
## Tainted handling
Any `Tainted` encountered during the walk is replaced with its
`pr-str` form (the redacted print-method output) — no rule needed.
## Privacy contract
`redact` never invokes `untaint` and never reads `:value` directly.
It is safe to compose into a logger appender.Structural redactor for ssh argv vectors.
Preserve the shape of the argv (every flag in its place, every
option preserved, count unchanged) while replacing Tainted slots with
stable hash tokens like <host:#a3f4> / <id:#7c2>.
The redactor is structurally aware: it understands the standard
ssh(1) flag syntax and knows which slots can hold a hostname /
identity / port. But it is intentionally CONSERVATIVE — it ONLY
touches slots whose contents are Tainted. Plain strings pass
through unchanged.
A hostname is whatever the user happens to call their VPS. We don't
want to pattern-match hostnames (false positives, false negatives).
Instead, the credential resolver / plan compiler decides what is
sensitive by wrapping in Tainted. The redactor's job is to take
that signal and present it as a token rather than a <redacted ...>
blob — preserving structural fidelity for debugging.
Single-arg flags (the next argv slot is the value):
-l <user> → identity -i <keyfile> → identity (keyfile path) -p <port> → port -o <key=val> → option (passed through; we don't parse it) -F, -E, -S, -W, -L, -R, -D, -J, -Q, -e, -m, -c, -O, -B (each of the above takes a value in the following slot)
Boolean flags (no following value): -1 -2 -4 -6 -A -a -C -f -G -g -K -k -M -N -n -q -s -T -t -V -v -X -x -Y -y
Anything that doesn't look like a flag and appears after we've consumed flags is treated as the destination (host, or user@host).
Plain strings: pass through verbatim.
Tainted slots: replaced with their <src:#hash> token form. If a
Tainted carries :source :argv-host, you get
<host:#xxxx>; for :argv-id you get <id:#xxxx>;
the source label IS the token prefix.
Repeated Tainted with the same underlying value within a single argv produces the SAME token (because the hash is value-stable within a JVM). Operators can spot "this argv references the same host twice" at a glance.
The function does NOT call untaint and does NOT lengthen or shorten the argv. Output count == input count.
Structural redactor for ssh argv vectors.
## Goal
Preserve the *shape* of the argv (every flag in its place, every
option preserved, count unchanged) while replacing Tainted slots with
stable hash tokens like `<host:#a3f4>` / `<id:#7c2>`.
The redactor is structurally aware: it understands the standard
ssh(1) flag syntax and knows which slots can hold a hostname /
identity / port. But it is intentionally CONSERVATIVE — it ONLY
touches slots whose contents are `Tainted`. Plain strings pass
through unchanged.
## Why conservative
A hostname is whatever the user happens to call their VPS. We don't
want to pattern-match hostnames (false positives, false negatives).
Instead, the credential resolver / plan compiler decides what is
sensitive by wrapping in `Tainted`. The redactor's job is to take
that signal and present it as a token rather than a `<redacted ...>`
blob — preserving structural fidelity for debugging.
## Recognized ssh flag idioms
Single-arg flags (the next argv slot is the value):
-l <user> → identity
-i <keyfile> → identity (keyfile path)
-p <port> → port
-o <key=val> → option (passed through; we don't parse it)
-F, -E, -S, -W, -L, -R, -D, -J, -Q, -e, -m, -c, -O, -B
(each of the above takes a value in the following slot)
Boolean flags (no following value): -1 -2 -4 -6 -A -a -C -f -G -g
-K -k -M -N -n -q -s -T -t -V -v -X -x -Y -y
Anything that doesn't look like a flag and appears after we've
consumed flags is treated as the destination (host, or user@host).
## Behavior
Plain strings: pass through verbatim.
Tainted slots: replaced with their `<src:#hash>` token form. If a
Tainted carries `:source` :argv-host, you get
`<host:#xxxx>`; for :argv-id you get `<id:#xxxx>`;
the source label IS the token prefix.
Repeated Tainted with the same underlying value within a single
argv produces the SAME token (because the hash is value-stable
within a JVM). Operators can spot "this argv references the same
host twice" at a glance.
The function does NOT call untaint and does NOT lengthen or shorten
the argv. Output count == input count.Tainted-value wrapper for sensitive material that must NOT reach an AI assistant or any debug surface — but whose presence and identity should still be observable via stable per-run hash tokens.
Where hive-system.secrets.core/Secret is the storage primitive for
credentials ("don't print, don't compare, don't serialize"), Tainted
is a diagnostic primitive: it wraps any value (hostnames, ports,
identities, env values, argv slots) so we can correlate the same
underlying value across many log lines with a short hash token like
<host:#a3f4> — without ever printing the value itself.
Tainted is a defrecord with three fields:
:value — the wrapped sensitive value :source — non-sensitive label (e.g. "pass:vps/r1/ip", :argv-host) :hash — 4-hex-char stable token derived from HMAC-SHA256(salt, value)
<host:#a3f4> across log lines.print-method ALWAYS emits <redacted src=... h=#xxxx> and NEVER
the underlying value. toString mirrors that.
untaint — the ONE blessed unwrap. Every call site is a leak risk.
This is the only safe extraction point and MUST only be invoked at
trust boundaries (process spawn, network write, file write to a
trusted local sink). Never call it within reach of a log appender,
event subscriber, or anything whose output may be surfaced to the
assistant.Secret apply.Tainted-value wrapper for sensitive material that must NOT reach an AI
assistant or any debug surface — but whose *presence and identity*
should still be observable via stable per-run hash tokens.
## Purpose
Where `hive-system.secrets.core/Secret` is the storage primitive for
credentials ("don't print, don't compare, don't serialize"), `Tainted`
is a *diagnostic* primitive: it wraps any value (hostnames, ports,
identities, env values, argv slots) so we can correlate the same
underlying value across many log lines with a short hash token like
`<host:#a3f4>` — without ever printing the value itself.
## Type
`Tainted` is a defrecord with three fields:
:value — the wrapped sensitive value
:source — non-sensitive label (e.g. "pass:vps/r1/ip", :argv-host)
:hash — 4-hex-char stable token derived from HMAC-SHA256(salt, value)
## Hash semantics
- Hash is computed once at construction and cached.
- Same value within a single JVM run produces the SAME hash token,
so operators can correlate `<host:#a3f4>` across log lines.
- Salt is generated per JVM at namespace-load time via SecureRandom
and held in a private atom — cross-JVM-restart unlinkability is by
design (the salt cannot be reconstructed from prior runs).
- Hash space is 16^4 = 65,536 — collision probability is acceptable
within a single run's working set of distinct sensitive values.
## Print safety
`print-method` ALWAYS emits `<redacted src=... h=#xxxx>` and NEVER
the underlying value. `toString` mirrors that.
## Audit surface
- `untaint` — the ONE blessed unwrap. Every call site is a leak risk.
This is the only safe extraction point and MUST only be invoked at
trust boundaries (process spawn, network write, file write to a
trusted local sink). Never call it within reach of a log appender,
event subscriber, or anything whose output may be surfaced to the
assistant.
## Non-goals
- Encryption / secure memory wiping. JVM strings can't be reliably
zeroed; the same caveats as `Secret` apply.
- Cross-process correlation. By design — see Hash semantics above.cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |