v2.63.0

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.

mask β€” visible, transformed, read-only deterministic β€” no LLM in the read path fail-closed v2.63.0+

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

1

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"
2

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
3

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.

PresetParamsWhat it doesGood 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:

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

SurfaceBehaviour
c3_readServes the view with its banner. Line/symbol selection is ignored β€” positions in a view don't map to the original.
c3_compressCompresses the view. Cache keys include the view hash, so a rule change can never be answered from a pre-mask cache entry.
c3_searchMasked files stay discoverable; snippets and the index come from the view. Results carry a [c3-mask:limited] footer.
c3_editRefuses β€” masked is read-only.
c3_shellRefuses 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_validateRefuses β€” type checkers quote source lines verbatim.
c3_impact, c3_filterRefuse β€” they depend on real line numbers and raw content.
c3_delegateRefuses β€” subprocess backends open the real file themselves.
Native Read/Grep/GlobDenied 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

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

SymptomCause & 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.