v2.145.1

Access Guard

Mark the paths an AI agent must never read, never write, or may only touch after you say so β€” and have every C3 surface enforce it. Four rule kinds in your project (or global) config, one evaluator, refusals that tell the agent exactly why and what to do instead.

deny β€” no read, no write, no enumerate read_only β€” no write confirm β€” write pauses for a human mask β€” transformed view (own guide)

What this is β€” and is not. Access Guard protects a cooperative agent against mistakes and prompt-injection: an agent that names a protected path gets a firm, reasoned refusal at every C3 surface. It is not a sandbox. It governs file actions invoked through C3's tools and hooks, but does not isolate raw OS processes or external editors. See Coverage for exactly where each layer holds.

Quick start

1

Add a rule

UI β†’ project β†’ Access Guard tab β†’ Add rule, or:

c3 access add "secrets/**" --kind deny
2

Or pause instead of block

A confirm rule lets writes through only after you approve them, one at a time:

c3 access add "infra/**" --kind confirm
3

Test a path

Use the tab's Test path probe, or:

c3 access check secrets/key.txt
4

Let it work

A deny/read_only attempt returns a firm [c3-access:denied] refusal. A confirm write pauses and asks you. See Confirm β€” ask me first.

Rules & semantics

Rules live in the access section of .c3/config.json (project scope) and ~/.c3/config.json (global scope):

{
  "access": {
    "deny":      ["secrets/**", "*.pem"],
    "read_only": ["docs/legal/**", "migrations/**"],
    "confirm":   ["infra/**", "*.tf"]
  }
}
Rule kindReadWrite / create / deleteAppears in search?
denyβœ—βœ—βœ— (never listed or hinted)
read_onlyβœ“ (unless another rule denies)βœ—βœ“
confirmβœ“ (reads are never gated)pauses for your approvalβœ“

There is a fourth kind, mask β€” reads stay open but come back transformed. It's its own subsystem with its own guide: Mask Guard.

Built-in protections

These apply in every C3 project with no configuration. Tier 0 is absolute: nothing turns it off. Tier 1 is on by default, but each guard now takes an independent mode: deny (tighten to a full block), confirm (pause and ask), allow (off), or default (the shipped behaviour). If the access config is corrupt or unparseable, that scope fails closed β€” everything in it is denied until fixed.

Tier 0 β€” absolute, no mode, no opt-out

PathsProtectionWhy
.c3/secrets.encdenyCredential vault, encrypted at rest
.c3/cred_state.jsondenyCredential registry sidecar
.c3/vault_backup.jsondenyPassphrase-sealed vault backup (v2.145.0)

These already carry their own guard (the vault's own encryption) and their own human-only escalation, so an opt-out here would only be a shorter route to the same secrets. c3 access builtin mode refuses to touch them at any scope, and even an approved **/.c3/** write or a confirm-mode .c3/** can never reach them β€” see Override Requests.

Tier 1 β€” mode-governable

PathsDefault modeWhat each mode does
**/.env*deny (all ops) confirm pauses every op, not just writes β€” a write-only confirm on a full-deny guard would silently become allow-read. allow turns it off.
**/.c3/** Β· **/.claude/settings*.json Β·
**/.git/** Β· **/.grok/hooks/**
read_only (write-deny) deny tightens to a full block; confirm pauses the write; allow turns the write-deny off. Reads stay open in every mode here.

**/.grok/hooks/** is Grok Build's hook registration (the command lines it runs) β€” the same role **/.claude/settings*.json plays for Claude Code.

Two keys, on purpose. Loosening a Tier 1 guard needs a config entry and a matching keyring attestation β€” either alone leaves the builtin enforcing. An agent that manages to write config.json (exactly the move a prompt-injected one would try) still cannot produce the attestation. Loosening **/.env* also prints one advisory line pointing at the vault: c3_credentials(action='import_env') reads the file server-side, so the agent gets names, lengths and fingerprints and never a value.

# every project on this machine
c3 access builtin mode "**/.env*" confirm

# THIS project only β€” replaces the global mode for that glob
c3 access builtin mode "**/.c3/**" allow --project

# back to shipped behaviour
c3 access builtin mode "**/.env*" default

A project-scope mode is attested under its own keyring realm, so a cloned repo that carries access.builtin_mode in its config changes nothing on a machine that never ran the attestation β€” the mode lives in this machine's keyring, not in the checkout. See Per-project & the Hub.

Tier 1, confirm-by-default β€” the agent-config surfaces

Until v2.100.0 these files were fully writable β€” only after-the-fact artifact capture watched them. An agent could add itself an MCP server, rewrite a hook's body, or edit its own instructions, and nothing paused it. Now every write to any of them holds for your one-tap approval; reads stay open, because an agent must always be able to read its own instructions.

GroupPaths
Instructions**/claude.md Β· **/agents.md Β· **/gemini.md Β· **/AGENTS.override.md Β· **/.cursorrules
MCP config**/.mcp.json Β· **/.vscode/mcp.json Β· **/.cursor/mcp.json Β· **/.codex/config.toml Β· **/.codex/hooks.json Β· **/.gemini/settings.json Β· **/.grok/config.toml
Claude Code bodies**/.claude/hooks/** Β· **/.claude/skills/** Β· **/.claude/agents/** Β· **/.claude/commands/** Β· **/.claude/plugins/**
Other IDEs**/.github/copilot-instructions.md Β· **/.grok/skills/** Β· **/.grok/agents/** Β· **/.grok/rules/**

**/.claude/settings*.json stays in the harder Tier 1 row above, not here: hook registration stays a full write-deny while hook bodies pause, because registration is what decides code execution. This tier is mode-governable too: deny hardens it to a write-deny and allow restores the pre-2.100 behaviour, through the same c3 access builtin mode. See Confirm β€” ask me first for what actually happens when a write here pauses.

Two more paths stay a plain, non-configurable write-deny: the C3 install directory (agent self-modification, installed layouts only) and the credential-vault write guard, which is unconditional and keys off a fixed tool set β€” no mode reaches it.

*.pem, id_rsa* and *.key are not builtins β€” they're a default rule seeded into global scope on install, visible in c3 access list and removable like any other rule.

Confirm β€” ask me first

A confirm rule is the middle mode between "always allowed" and "always refused": the agent may change these files, but every change goes past you first. Reads are untouched. A write refuses softly, and C3 itself (not the agent) files an Override Request the moment the write is blocked.

c3 access add "infra/**" --kind confirm

What happens when a write hits a confirm rule

  1. The write is refused with tag [c3-access:confirm] instead of your file being touched.
  2. C3 auto-files the request. The agent cannot forget to ask and cannot word the ask β€” the card is built from the trusted denial (tool, operation, path, rule), never from anything the agent composed. This happens at the PreToolUse hook for native tools and inside c3_edit; every other c3_* tool refuses without filing and says so.
  3. You decide on your phone, in the Hub, or with c3 override approve <id> β€” see Override Requests for all three.
  4. One tap mints a single-use, session-bound, path-exact grant. The agent retries the exact same call, on the same surface β€” a grant matches on tool, so a hook-filed native Edit hold is not satisfied by a c3_edit retry.

The refusal names the request and tells the agent what to do β€” wait on the id (c3_override(action='wait', request_id=..., timeout_s=180)), that a "still pending" answer is not a denial, and, when no request could be filed, to follow that reason's own instruction (a deny+mute means don't ask again; a rate limit means withdraw or wait) rather than defaulting to "ask in chat."

Precedence

confirm is the loosest non-allow outcome: a hold can be approved into a write, a read_only cannot. So deny > mask > read_only > confirm. A user's read_only rule is never shadowed by the agent-config confirm tier, and a confirm rule you write over **/.c3/** does not outrank the Tier 1 **/.c3/** write-deny. Use c3 access builtin mode "**/.c3/**" confirm for that instead (Built-in protections).

access_confirm defaults on

Override Requests as a whole default off β€” a project has to opt in before an agent may ask for anything. Confirm rules are the one deliberate exception: writing a confirm rule is itself the opt-in, so the access_confirm layer defaults on and doesn't need override.enabled. Turn it off explicitly with override.layers.access_confirm: false if you want confirm rules to refuse silently, with no auto-filed request, instead.

Shell writes pause too

A confirm rule (and read_only, and the Tier 1 write-denies) governs the write class, and since v2.102.0 that includes c3_shell and native Bash, not just c3_edit. echo >> CLAUDE.md, sed -i on .mcp.json, or a heredoc into a hook body all evaluate the files the command writes as a write and pause the same way an editor write would. See Shell command scan for how the scan finds those targets, and its limits.

Override Requests

Every guard on this page is terminal by default: the only way past a block used to be a human weakening a rule permanently. Override Requests add the missing primitive β€” a grant that lets one blocked call succeed once, without touching policy. The rule that denied the call is still in force the instant the grant is spent.

Off by default, one layer excepted. Every escalatable layer defaults to off. A project must opt in with override.enabled and turn on the specific layer, except access_confirm, which is on because a human writing a confirm rule already is the opt-in.

What can ever be escalated

LayerWhat it covers
disciplineNative Edit/Write blocked by tool discipline (not path policy)
access_readonlyA read_only rule
access_denyA user deny rule β€” needs the glob retyped by hand to approve
access_builtinA Tier 1 builtin β€” same typed-glob challenge
access_confirmA confirm rule β€” one tap, no typed challenge (default ON)
maskNot really escalatable in practice β€” a masked path is a different view being served, not a refusal waiting to be lifted
shell_warnThe c3_shell soft-warn caveat β€” replaces the warning line, never unblocks the catastrophic-command tier

Never escalatable, at any setting, by any approval: the credential vault, .c3/secrets.enc, .c3/cred_state.json, .c3/vault_backup.json, the Tier-0 absolute denies, the dispatcher's fail-closed deny, and the catastrophic c3_shell blocks. A denial from one of those never reaches the grants file, and the refusal never mentions that a request surface exists.

The loop: ask, decide, retry

1

Agent asks

Blocked, escalatable, opted-in: the agent calls c3_override(action='request', path=..., why=...). Rate limited: 3 pending per session, 20/hour per project. A duplicate ask returns the existing card.

2

You decide

Phone, Hub, or CLI β€” see below. Approving access_deny or access_builtin needs the rule glob retyped by hand; every other layer is one tap.

3

Agent retries

The grant is single-use, session-bound, path-exact and short-lived (15 min by default). The agent must wait or poll (c3_override(action='wait', request_id=..., timeout_s=180)), then retry the exact call once.

The agent's tool β€” c3_override

actionDoes
requestFile a request for one blocked path (+ op, why)
statusCheck a request_id without blocking
waitBlock up to timeout_s (default 60, max 180) for a decision
listThis session's requests
withdrawCancel a request this session filed

There is no approve action, anywhere in this tool, on purpose. Asking for one by name gets a refusal that says so β€” approval is human-only, from the phone, the Hub, or c3 override approve.

Who answers

Mute

c3 override deny <id> --mute denies and suppresses identical requests (same project, session, layer, rule, tool, operation and path) for the rest of the session. A new session gets to ask again. The mute store fails open (the opposite of the grant store): a lost mute costs one extra notification, never a capability.

Session and rule-scoped grants

The default grant (mode=once) covers one exact call. Two looser modes exist, both opt-in and both needing their own typed confirmation:

ModeCoversNeedsLifetime
onceThis exact tool + path, one useNothing extraUp to 15 min
sessionUnlimited uses of this exact call, this sessionoverride.allow_session_grants + retype sessionUp to 15 min
ruleEvery path the rule matches, any tool in the same op class (read/write/shell)override.allow_rule_grants + retype the rule glob4h default, 8h hard ceiling, or 30 min idle β€” whichever comes first

A rule grant relaxes exactly two of the nine match conditions: the exact tool becomes any tool in the same declared class (an unclassed tool, like c3_artifacts or c3_project, never widens), and the exact path becomes the path set the rule glob describes. It still cannot reach a Tier-0 file or the override policy/grant store, even under a **/.c3/** rule β€” proven live: such a grant covers .c3/notes.txt and never override_grants.json. Rule grants are visible and killable, not just logged: the Hub's Access tab lists active grants with what each one reaches, and can revoke one.

What it costs you

A rule that keeps generating requests is a signal, not just noise. c3 override costs [--days N] [--all] and the Hub's "costing you" strip surface it: how many times a rule held the agent in the trailing window, how you answered, and a suggestion β€” convert to allow (mostly approved), tighten or deny (mostly denied), or review.

c3 override costs --days 7

Per-project control & the Hub

Two things used to be global-only and are now settable per project: which builtin guards apply, and what override policy a project runs. A third is new outright: editing Access Guard rules across every registered project from one screen.

A project can loosen (or tighten) its own builtins

Global scope used to be the only place that could change a Tier 1 builtin's mode. A project-scope mode now replaces the global one for that glob, loosening included β€” attested under its own keyring realm (builtin_mode|proj|<path>|<glob>), so cloning the repo never carries the loosened mode to a machine that never attested it.

c3 access builtin mode "**/.env*" confirm --project
c3 access list   # prints [project] / [global] beside each mode

Inherit again, or see one project's whole policy in one read

Setting a mode or an override policy at project scope used to be a one-way trip: nothing meant "go back to whatever global says" except hand-editing the config out. c3 override policy and the enforcement mode now both take a clear, which drops the project's own section and falls back to global (and global falls back to the built-in default). The Hub reads a project's whole effective override policy for every key, whether it's corrupt or even initialized, in one call instead of one request per key.

Access Guard rules across every project, from the Hub

The Hub's Access tab can add or remove a deny / read_only / confirm rule on any registered project, or on global scope, without opening that project. Adding tightens, so it needs no confirmation; removing a deny rule, or any global-scope rule, needs the glob retyped as a confirmation. A folder with no .c3/ is refused rather than silently initialized β€” a rule C3 does not read protects nothing while claiming to. Every real write lands in the target project's own edit ledger.

"May agents ask?"

The Hub's Access tab also exposes override.enabled itself (the switch that decides whether a request may exist at all), with the seven escalatable layers listed individually and the same two caveats always on screen: policy merges by tightening only, and the credential vault and the catastrophic blocks are never escalatable at any setting. Turning the feature on, or turning a layer on, needs a typed confirmation; turning either off never does.

Shell command scan

c3_shell and native Bash (via the PreToolUse hook) both read a command's tokens and judge each one against Access Guard, per command segment (split on &&, ||, ;, |, and newlines) so a leading cd updates which directory later segments resolve against. Reads are checked against deny-kind rules only; since v2.102.0, the files a command writes (redirects, tee, sed -i, cp/mv, inline Python open(...,'w')) are extracted and evaluated as writes, so a read_only, confirm or write-deny rule holds a shell write exactly like an editor write would.

Best-effort, stated plainly in the refusal. Subshells, variables and globs are invisible to a token scan. A clean scan is not enforcement β€” it is mistake-and-injection containment. It errs toward under-flagging: a token it misses is still caught by the real path check on the tool that follows; a token it flags wrongly is a hard deny on a command that may name no file at all.

Spelling rules (no glob β€” a refusal cites these by name)

NameMeans
<unc>UNC / network path outside the project
<unresolvable>Path could not be resolved
<empty-component>A path component empties after normalization
<8.3-alias>8.3 short-name component on a non-existing target
<ads>NTFS alternate-data-stream syntax (Windows only)

<ads> caused the most false positives while the scan matured, all fixed and worth knowing if a command you expect to run gets denied by a rule c3 access list doesn't show:

Refusal messages

Every denial carries a stable machine tag, the matched rule and scope, and explicit no-retry guidance β€” so the agent pivots instead of retry-looping or silently recreating "missing" files.

TagMeaning
[c3-access:denied]Operation refused by a deny rule (or a write refused where read_only applies via hooks). Policy, not an error β€” the agent is told not to retry or route around it.
[c3-access:read_only]Write refused; reads remain separately evaluated.
[c3-access:confirm]"S8": a pause, not a refusal. Names the auto-filed request id and the exact c3_override(action='wait', ...) call, or says why no request was filed and what to do about it. See Confirm β€” ask me first.
[c3-access:limited]Search/listing footer: results may omit protected paths β€” a file that doesn't appear may still exist.
[c3-access:error]The enforcement hook itself failed β€” native write tools are denied until it loads (fail-closed). See Troubleshooting.
[c3-override:granted]An approved grant covered this retry β€” the caveat line an agent sees in place of a block.
[c3-override:spent]A grant existed but was consumed by a concurrent call between the check and the write; the retry is refused rather than let through ungranted.
[c3-override:not-escalatable]A c3_override(action='request') for a layer that can never be asked about (vault, Tier-0, catastrophic shell). Never shown for a denial that didn't offer to escalate.

Masked-path tags ([c3-mask:transformed], [c3-mask:limited], [c3-mask:unsupported]) are documented on the Mask Guard page.

Coverage β€” honest limits

Enforced: C3 MCP tools (any agent that uses C3: Claude Code, Codex, Gemini, Grok Build, Oracle-connected models) Β· Claude Code native tools (PreToolUse hooks, fail-closed) Β· c3_shell and native Bash (best-effort per-segment token scan: reads against deny rules, writes against every write-class outcome; see Shell command scan).

Not enforced: a non-Claude agent's raw shell or direct file APIs (including native file tools in non-Claude CLI environments unless they route through C3 tools), external editors, and OS-level access. Renames/moves via shell commands are a known v1 gap. In an IDE with no PreToolUse hooks (Codex, Antigravity, Copilot) only the c3_* tools are covered; a native write there is not intercepted. Access Guard is a guardrail for cooperative agents. Pair it with OS permissions when you need a real boundary.

Tool discipline is a separate knob

Access Guard governs paths. A neighboring layer, tool discipline (c3 enforce), governs whether native Edit/Write are blocked in favour of c3_edit at all β€” a workflow preference, not a security boundary. Loosening it never reaches Access Guard rules, the credential vault, or agent locks; those enforce at every discipline mode.

c3 enforce                    # show the current mode
c3 enforce advisory           # allow native writes, with a nudge
c3 enforce strict             # block until a c3_* call ran first
c3 enforce off                # no nudging
c3 enforce --global advisory  # default for every project on this machine

c3 access stats labels each denial with its layer (path policy vs. tool discipline) and names the exact command that clears it. Full reference: docs/enforcement.md.

CLI commands

# rules
c3 access list                                    # rules by scope, builtins, coverage
c3 access add "secrets/**" --kind deny            # project scope
c3 access add "infra/**" --kind confirm           # pause writes, don't block them
c3 access add "*.pem" --kind deny --global        # global scope
c3 access remove "secrets/**" --kind deny
c3 access check secrets/key.txt                   # probe: verdict + matched rule
c3 access check docs/x.md --op write
c3 access stats                                   # what got denied, how often, how to clear it
c3 access stats --clear

# builtins
c3 access builtin mode "**/.env*" confirm         # deny | confirm | allow | default
c3 access builtin mode "**/.c3/**" allow --project
c3 access builtin disable "**/.git/**"            # legacy spelling of mode allow
c3 access builtin enable "**/.git/**"

# override requests (see Override Requests above)
c3 override policy                                # effective policy + escalatable layers
c3 override requests [--status pending] [--all]
c3 override approve <id> [--mode once|session|rule] [--confirm <glob>]
c3 override deny <id> [--mute]
c3 override list [--session <id>] [--audit N]  # live grants
c3 override grant <path> --session <id> [...]  # mint one directly, no request
c3 override check <path> --session <id>        # would a grant cover this now?
c3 override revoke <grant_id>
c3 override sweep                                 # drop expired/spent grants
c3 override costs [--days 7] [--all]

# Mask Guard β€” see masking.html
c3 access mask add "data/**" --preset sample_rows --params "count=20,strategy=first"

UI & probe

Project Access tab

Rules grouped by scope (builtins shown with their mode, not just locked), an add-rule form with all three glob-list kinds, delete with typed confirmation for deny rules, a mode selector and scope picker for builtins, and a Test path probe that shows the verdict, matched rule, and the exact refusal the agent would see. The coverage matrix is pinned at the bottom of the tab.

Hub Access tab (desktop, cross-project)

The Hub edits policy and rules; it does not replace the per-project UI β€” day-to-day rule authoring still happens in the per-project Access tab or c3 access.

Troubleshooting

[c3-access:error] on every write The access hook failed to load β€” this is fail-closed by design. Check .c3/hook_errors.log, rerun c3 install-mcp. C3 MCP tools keep working meanwhile.
Everything in a scope denied That scope's access section is corrupt or has an unknown key. Fix the JSON; c3 access list names the scope.
Expected file "missing" in search Check c3 access check <path> β€” deny rules remove files from search entirely (deny-enumerate).
Rule not matching Globs are POSIX-style: use forward slashes; remember * does not cross directories β€” use **. Probe with c3 access check.
Agent says "wait" never returns / times out The default c3_override wait is 60s; the agent should pass timeout_s=180 and treat "still pending" as not-a-denial, waiting again rather than giving up.
A retry after approval is still refused A grant matches the tool that filed the request β€” retry the same surface (a hook-filed native Edit hold needs a native retry, not c3_edit). Check c3 override list for whether the grant still has uses left.
"May I ask?" offer never appears in a refusal Override Requests are off by default; a project must set override.enabled and turn the specific layer on (c3 override policy shows both). The offer line only ever appears on the hook surface, not on a bare c3_* refusal.
A builtin mode won't stick Both a config entry and a keyring attestation are required; c3 access builtin mode writes both together. A hand-edited config entry with no matching attestation is ignored and the builtin keeps enforcing its shipped default.

Relationship to the Credential Vault. Credentials protect values (never in agent context, injected at the subprocess boundary); Access Guard protects files and folders. The vault's registry and sidecars are covered by Access Guard Tier 0, and both systems log every mutation to the same edit ledger.