Mask Guard
Expose a file or folder to the agent, but control what it actually sees. Redact secrets, pseudonymise columns, keep 20 rows of a million-row CSV, or serve a vendor tree as signatures only β deterministically, on every read, through every C3 surface.
Access Guard answers a yes/no question: may the agent touch this path?
Mask Guard answers a different one: what should the agent see when
it does? A deny rule hides a file. A mask
rule keeps it discoverable and searchable, but every byte the agent
receives comes from a materialized view β a transformed
copy built once, validated, and stored under
~/.c3/masked/. Your real file is never modified.
Context hygiene, not containment. The real bytes stay
on disk. Your editor, a raw shell, a non-Claude agent, and
git all still see them. Mask Guard governs what reaches the
model through C3. If a file must never be readable at all, use
deny β see Access Guard.
Quick start
Add a mask rule
UI β project β Access Guard tab β + Add mask, or:
c3 access mask add "data/**" \
--preset sample_rows \
--params "count=20,strategy=first"
Preview it
See exactly what the agent will see, on your real file, before you trust the rule:
c3 access mask preview data/customers.csv
Activate
Purges pre-mask caches, index, file memory and facts, then builds and validates every view:
c3 access mask activate
What the agent sees afterwards:
Your file
id,name,email,plan
1,Ann Chen,ann@acme.io,pro
2,Bo Diaz,bo@acme.io,free
β¦ 412,908 more rows
What the agent sees
[c3-mask:transformed] view=sampled data/customers.csv
β this is a policy-transformed view, not the
original fileβ¦
id,name,email,plan
1,Ann Chen,ann@acme.io,pro
2,Bo Diaz,bo@acme.io,free
# Β«c3:redacted:rowsΒ» 412,906 of 412,908 data rows
withheld by mask policy (sample_rows:first:2)
Presets
Every transform is a named, versioned preset. There is deliberately no free-form recipe language and no classifier in the read path: the same file must render identically on every read and through every tool, or an agent could recover the original by comparing surfaces.
| Preset | Params | What it does | Good for |
|---|---|---|---|
| redact_secrets | β | Replaces detected secrets β AWS keys, GitHub/OpenAI/Slack tokens,
JWTs, PEM blocks, connection strings, *_PASSWORD=
assignments β plus a high-entropy sweep, with inert placeholders.
Surrounding code stays readable. |
Config files, .tfvars, logs, fixtures |
| redact_columns | columns |
Replaces the named CSV/TSV columns with stable salted pseudonyms. The same real value always maps to the same pseudonym, so joins, uniqueness and cardinality survive β but there is no reverse map anywhere to steal. | Customer data, exports, PII-bearing tables |
| sample_rows | count, strategy (first|last) |
Keeps the header plus N data rows and states how many were withheld, so the agent cannot mistake the sample for the whole. | Huge CSVs, log tails |
| signatures_only | β | Structure only β declarations and signatures, no bodies. Reuses C3's compressor, so it is also a large token saving. | Vendored SDKs, generated clients, huge modules |
Protected Mode. After rendering, C3 re-scans the output for secret material. If anything survived, the read is refused rather than served. A detector that quietly missed something becomes a loud error instead of a silent leak.
Two rules matching the same path with different presets is a configuration error, not a precedence puzzle β that path fails closed until you remove the conflict. Rule order never affects output.
Activation β why a rule isn't enough
Before you masked that file, C3 had already read it: its content is in the compression cache, the search index, file memory, the repo map, and possibly in auto-memory facts. A new rule does not retroactively clean any of that. So adding a mask rule starts a transaction:
pending β purge derived artifacts β build + validate views
β rebuild indexes β active
Until it completes, the UI banner and c3 access mask status
both say so in plain terms. C3 will not claim a protection it has not
actually established.
First activation clears unknown-provenance facts.
From v2.63.0 every auto-memory fact records which files it came from.
Facts written by older versions have no provenance β they cannot be
proven clean, so the first activation in a project deletes them
wholesale. Facts you wrote by hand with c3_memory are
unaffected.
Masking is forward-only within a session. A rule added mid-session cannot unread what the agent already has in its context. Start a fresh session for full effect.
Why masked always means read-only
A masked path refuses every write, with no override. This is not conservatism β it is the only sound option. The agent read a transformed file, so any edit it proposes is expressed in transformed coordinates:
- Cropped rows have no inverse β
sample_rowsdiscarded them, so there is nothing to map back to. - The agent may invent an identifier that has no real counterpart.
- A half-copied placeholder would be written to your file verbatim β silently corrupting the one file you cared enough to protect.
- An edit can be perfectly correct against 20 sampled rows and wrong for the other 412,906.
Placeholders are deliberately spelled Β«c3:redacted:kindΒ» β
not valid in any language β so that even in the impossible case where
one reached a real file, the compiler or linter trips immediately
instead of the value being silently committed.
What each tool does with a masked path
| Surface | Behaviour |
|---|---|
| c3_read | Serves the view with its banner. Line/symbol selection is ignored β positions in a view don't map to the original. |
| c3_compress | Compresses the view. Cache keys include the view hash, so a rule change can never be answered from a pre-mask cache entry. |
| c3_search | Masked files stay discoverable; snippets and the index come from the view. Results carry a [c3-mask:limited] footer. |
| c3_edit | Refuses β masked is read-only. |
| c3_shell | Refuses content reads over masked paths. Post-filtering stdout is not offered: it could strip a secret but could never reconstruct a crop, so it would be a guarantee C3 cannot make. |
| c3_validate | Refuses β type checkers quote source lines verbatim. |
| c3_impact, c3_filter | Refuse β they depend on real line numbers and raw content. |
| c3_delegate | Refuses β subprocess backends open the real file themselves. |
| Native Read/Grep/Glob | Denied by the PreToolUse hooks β they cannot render a view. |
What the agent is told
Loud about evidence quality, quiet about your policy. Every transformed
payload starts with a banner naming the rule and a coarse view class
(redacted, sampled, structure_only),
because how a view is incomplete changes what conclusions are
safe. The agent is never told what was removed or where the real value is.
The alternative β masking silently β is worse: an agent that
confidently reasons over synthetic data and ships code built on it, with
neither of you aware. Stable machine tags:
[c3-mask:transformed], [c3-mask:limited],
[c3-mask:unsupported].
Honest limits
- Real bytes remain on disk β editors, raw shells, non-Claude agents and git see them.
- Git history is not masked.
- Masking is forward-only within a live session.
- Secret detection is pattern-based and has false negatives; Protected Mode converts most of those into refusals, not all.
- Binary and non-UTF-8 files cannot be rendered β they refuse rather than fall back to raw.
- Pseudonyms are one-way and salted per project; the salt lives in
~/.c3/masked/<project>/saltand never enters model context.
CLI commands
# add / replace a rule (--global for all projects)
c3 access mask add "data/**" --preset sample_rows --params "count=20,strategy=first"
c3 access mask add "*.env.txt" --preset redact_secrets
c3 access mask add "people.csv" --preset redact_columns --params "columns=email,full_name"
c3 access mask add "vendor/**" --preset signatures_only
# inspect
c3 access list # all rules, all scopes, + coverage
c3 access mask status # activation state, failures
c3 access mask preview data/people.csv # exactly what the agent sees
c3 access check data/people.csv # verdict for one path
# apply / remove
c3 access mask activate [--reindex]
c3 access mask rm "data/**"
Troubleshooting
| Symptom | Cause & fix |
|---|---|
| Banner says configured but not activated | The rule is on disk but derived artifacts still hold pre-mask content. Run c3 access mask activate. |
| Status INCOMPLETE | At least one file could not be rendered, so it now refuses reads. c3 access mask status lists each failure. Usually a redact_columns column name that doesn't exist in the header, or a binary file caught by a broad glob. |
Agent reports [c3-mask:unsupported] |
Working as designed β that surface (shell, validate, impact, delegate) cannot render a view. Use c3_read, or narrow the glob if the file doesn't need masking. |
signatures_only refuses |
Unsupported language or unparseable syntax. It fails closed rather than emit the original. Use redact_secrets instead. |
| Scope shows corrupt | An invalid mask entry β unknown preset or bad params. That scope evaluates deny-all until you fix config.json. c3 access mask add validates before writing, so this usually means a hand edit. |
| Pseudonyms changed after a rebuild | The salt file was deleted. It lives in ~/.c3/masked/<project>/salt and is preserved across mirror clears. |
Design rationale, leak-channel analysis and residual risks:
docs/mask-guard.md in the repository.