combra-guard
A config-driven policy gate for AI coding agents. You write rules once in guard.toml;
combra-guard enforces the same rules at four points — from an instant in-editor deny up to an
authoritative CI wall. The engine hardcodes no repo policy: the four generic rule types
(forbidden_field, forbidden_pattern, protected_path, protected_read) are parameterized per
repo in guard.toml.
| Gate | Trigger | Covers | Strength |
|---|---|---|---|
Action (Claude PreToolUse) |
writes; protected_read on Read/Grep; literal Bash reads/writes to a protected path |
Claude only | instant write seatbelt; prevention-only read guard |
Turn (Claude Stop) |
write result vs turn-start baseline | Claude only | write seatbelt — catches Bash writes by result |
Commit (git pre-commit) |
staged writes on git commit |
any agent + human | write seatbelt, --no-verify-bypassable |
Merge/CI (check-diff) |
writes in the PR diff | any agent | the authoritative write wall |
The Action, Turn, and Commit gates are seatbelts an agent or a human can route around; the CI wall is
the authoritative write wall. See docs/DESIGN.md for the architecture and
per-phase build notes.
Install
combra-guard is not yet published to PyPI. For now, install from a clone:
pipx install <path-to-clone> # or:
uv tool install --from <path-to-clone> combra-guard
Once it is released, pipx install combra-guard / uv tool install combra-guard will work.
Zero runtime dependencies (stdlib only). Python 3.11+.
Quickstart
combra-guard init # scaffold guard.toml, wire the Claude hook + git commit gate
combra-guard selftest # run the hermetic golden cases
init is idempotent. It:
- writes a starter
guard.tomlat the repo root (including an activeprotect-guard-configrule that guardsguard.toml,guard.remote.toml,.claude/settings.json, and.githooks/**, so an agent can't disable the guard in-session); - adds the
Edit|Write|MultiEdit,Read|Grep, andBashPreToolUsehooks plus the turn-gateUserPromptSubmit/Stop/SubagentStophooks to.claude/settings.json, never clobbering hooks you already have; - compiles
protected_readpaths into Claude's nativepermissions.deny; - installs the git commit gate and appends
guard.remote.tomlto.gitignore.
To wire the Action hook by hand instead:
{ "hooks": { "PreToolUse": [
{ "matcher": "Edit|Write|MultiEdit", "hooks": [{ "type": "command", "command": "combra-guard hook" }] },
{ "matcher": "Read|Grep", "hooks": [{ "type": "command", "command": "combra-guard hook" }] },
{ "matcher": "Bash", "hooks": [{ "type": "command", "command": "combra-guard hook" }] }
] } }
The hook reads $COMBRA_GUARD_CONFIG, or the repo-root guard.toml (found by walking up from any
subdirectory). Author rules directly, or pull governed rules
from a Combra workspace.
Usage
Five runnable examples: a guard.toml snippet, the command that trips it, and the line combra-guard
prints.
1. In-editor deny on a forbidden pattern
[[rules]]
id = "no-console-log"
type = "forbidden_pattern"
globs = ["src/*.js"]
pattern = "console\\.log"
cite = "no debug logging in shipped code"
deny_reason = "'{line}' is a debug log; remove it before shipping."
When Claude tries to Edit src/app.js to add console.log(x), the PreToolUse hook denies it
before the write lands. Claude sees:
combra-guard[no-console-log]: 'console.log(x)' is a debug log; remove it before shipping. — no debug logging in shipped code
The {line} placeholder is the added line (stripped); forbidden_pattern fires only on lines the
edit adds, so existing console.logs are grandfathered.
2. The CI wall
check-diff --base <ref> judges the whole PR diff in one process and exits non-zero on any
violation — no in-diff trick bypasses it; close the workflow-rewrite hole with branch protection +
CODEOWNERS on .github/workflows/ (see Honest limits). Wire it as a
required status check with a single step (full workflow in the CI wall section):
run: combra-guard check-diff --base "origin/$BASE_REF"
A PR that adds the console.log from example 1 fails the check:
combra-guard[no-console-log]: 'console.log("debug")' is a debug log; remove it before shipping. — no debug logging in shipped code (src/app.js)
The CI variant appends the file path so a multi-file scan names which file to fix. If you pull governed rules, add the pull step before this one (see the full CI section below).
3. The commit gate
init installs .githooks/pre-commit, which runs combra-guard check-diff --staged (staged index
vs HEAD). A git commit that stages the same violation is blocked:
$ git add src/app.js && git commit -m "add log"
combra-guard[no-console-log]: 'console.log("debug")' is a debug log; remove it before shipping. — no debug logging in shipped code (src/app.js)
It fires for anything that commits — Claude, Codex, Cursor, or a human — with no per-agent wiring.
The non-zero exit and printed reason tell an agent what to fix; git commit --no-verify is the
honest bypass.
4. Block a secret read
[[rules]]
id = "no-secret-reads"
type = "protected_read"
paths = ["~/.ssh/**", ".env"]
cite = "secrets never enter model context"
deny_reason = "reading {path} is blocked"
A Claude Read of ~/.ssh/id_rsa is denied before the file enters model context:
combra-guard[no-secret-reads]: reading /Users/you/.ssh/id_rsa is blocked — secrets never enter model context (/Users/you/.ssh/id_rsa)
{path} resolves to the canonical absolute path, so home paths, repo-relative paths, and symlinks
resolving outside the repo all match without the file being opened. init also compiles these paths
into Claude's native permissions.deny (e.g. Read(~/.ssh/**)). See read-guard for
the limits.
5. Pull governed rules from Combra
Manage rules centrally and pull them into each repo:
combra-guard init --remote https://api.combra.ai
export COMBRA_GUARD_TOKEN=<your Combra PAT>
combra-guard pull # GET {url}/v1/guard/rules → writes guard.remote.toml
init --remote adds a [remote] block naming the URL and the env var holding the token — the token
itself never lands in a file:
[remote]
url = "https://api.combra.ai"
token_env = "COMBRA_GUARD_TOKEN"
pull writes the fetched rules to guard.remote.toml (git-ignored, marked # GENERATED — do not edit) and refreshes the native read deny-list. Both guard.toml and guard.remote.toml are loaded
and enforced together; a rule id that collides across the two fails closed. pull requires https
(plain http only for localhost), never follows redirects, and validates every rule before
writing — a bad response leaves your existing rules untouched.
The four gates
Action gate
The PreToolUse hook judges one prospective Claude tool call and denies before it runs. It covers
Edit/Write/MultiEdit writes against all rule types, Read/Grep against
protected_read, and Bash against the write tripwire and the
keychain read tripwire. It fails open on combra-guard's own infra errors (never block the agent
on our breakage) but fails closed on a detector error while evaluating a matched file.
Turn gate
A Claude-only gate that closes the Action gate's biggest hole: a guarded file rewritten through
Bash (sed -i, > redirect, python - <<EOF) never touches the Edit/Write/MultiEdit
matcher. Instead of watching tools, the turn gate diffs the result:
- On
UserPromptSubmitit snapshots the turn-start state — the trusted rules plus the content of every guarded file (including git-ignored control files likeguard.remote.toml, and anyallow_sourcea rule references) — into<git-dir>/combra-guard/turn-<session_id>.json. - On
Stop/SubagentStopit re-reads those files and scans(baseline → working tree)with the snapshot's rules, blocking the Stop once and naming the file to revert.
The snapshot is the trusted side, so a turn that edits guard.toml or widens an allow_source
cannot re-authorize itself. The baseline lives under the per-worktree git dir, so it is never
committed and a linked worktree resolves correctly.
Honest limits. Claude-specific (it uses Claude Code's hook events). It blocks once — after
the block, stop_hook_active lets the turn end even if the violation is unfixed; the working tree
still holds it (the commit and merge gates backstop). A turn you interrupt before Stop is
unscanned. A resumed session with no baseline is skipped with a warning — the gate never diffs
against HEAD, so pre-existing local edits are never blamed on the agent. Human edits made in
parallel with the agent's turn are in the working tree but not the baseline, so they can false-block;
the reason names the exact file so you can see it's yours. combra-guard pull refreshes the current
session's baseline for the file it rewrites, so a mid-turn pull does not false-block.
Commit gate
init installs a tracked .githooks/pre-commit hook and points git config core.hooksPath at it.
The hook runs combra-guard check-diff --staged — it judges the staged index vs HEAD and
exits non-zero on a violation, so git commit is blocked. It fires for anything that commits, with
no per-agent integration.
# .githooks/pre-commit
#!/bin/sh
# combra-guard commit gate
combra-guard check-diff --staged || exit 1
- Installed via
core.hooksPath, not.git/hooks..git/hooksisn't shared by a clone;.githooks/is tracked, so cloning + oneinitarms every checkout. - Never-clobber. If
core.hooksPathalready points elsewhere, or a foreignpre-commitalready lives in.githooks/,initprints the one line to add (combra-guard check-diff --staged || exit 1) and skips. - Partial staging (
git add -p) is judged correctly — the index is exactly what will commit. - First commit (unborn
HEAD): every staged file is treated as newly added; rules load from the worktreeguard.toml. The first commit that introduces the control files (guard.toml,.githooks/**,.claude/settings.json) must bootstrap withgit commit --no-verify— it lands through a reviewed PR anyway.
Honest limits. A seatbelt, not the wall. git commit --no-verify bypasses it, and editing
.githooks/pre-commit or core.hooksPath (which lives in .git/config, where no file rule can
reach) disables it locally — the in-repo protect-guard-config rule catches a tracked-hook edit at
the merge wall, but a --no-verify bypass is honest and undefeatable by design. It is not installed
until someone runs init in the clone.
- Missing binary blocks commits. The gate fails closed: with
combra-guardoffPATH, the hook'scommand not foundexits non-zero, so everygit commitis blocked (not a silent no-op) until you install it — orgit commit --no-verifyin the meantime. - Linked worktrees.
core.hooksPathis relative, resolved per worktree. A worktree on a branch that predates the gate has no.githooks/, so git runs no hook there. Re-runinitthere to arm it. - Symlinked
.githooks. If.githooksis a symlink, the hook write follows it to its target;initdoes not replace the link with a directory.
CI wall
combra-guard check-diff is the authoritative, agent-agnostic wall: it judges a whole PR diff in one
process and exits non-zero on any violation. No Bash trick bypasses it, and a required status check
gates the merge.
combra-guard check-diff --base <ref> # PR wall: merge-base(<ref>, HEAD)...HEAD
combra-guard check-diff --staged # commit gate: staged index vs HEAD
- Three-dot merge-base diff.
--basediffsmerge-base(<ref>, HEAD)...HEAD, so the PR is judged as the change that will actually merge. A violation added and reverted within the branch does not fire; base-branch drift after the branch point is not blamed on the PR. - Rules load from the trusted (merge-base) side. A PR that deletes or empties
guard.tomlis still judged by the pre-PR rules — it cannot disable the wall from inside the same PR. Anallow_source(e.g.ENV_PROFILES) is likewise resolved from the merge-base side: a PR that adds a forbidden field and registers its name in the allow-source in the same diff still fires (register it in its own PR first).guard.remote.tomlis git-ignored, so CI re-runscombra-guard pullto fetch governed rules fresh — a neutered local copy never reaches the wall. - Renames. A rename from an unguarded path into a guarded one is treated as newly-added content
(smuggling fires); a rename within guarded scope keeps grandfathered fields; a rename of a
protected_pathfile out of guarded scope, and a plain deletion of one, both fire. - Binaries and symlinks. A file is read only if its path matches a rule (a PNG elsewhere in the PR never bricks the wall). A guarded path that is binary, oversized (>5 MB), or a symlink fails closed with a message telling you to narrow the rule's globs.
The full CI workflow, with the optional governed-rules pull:
# .github/workflows/guard.yml
name: guard
on: pull_request
jobs:
wall:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 } # base + head needed for the merge-base diff
- uses: astral-sh/setup-uv@v5
# combra-guard is not yet on PyPI; install from the clone. Once published:
# uv tool install combra-guard
- run: uv tool install --from . combra-guard
# Pull only when a [remote] is configured; a real pull failure reddens CI (no `|| true`) so a
# down Combra / revoked token can't silently drop the governed rules the wall trusts.
- name: Pull governed rules from Combra
env: { COMBRA_GUARD_TOKEN: ${{ secrets.COMBRA_GUARD_TOKEN }} }
run: |
if grep -q '^\[remote\]' guard.toml 2>/dev/null; then combra-guard pull; fi
- name: Enforce guard rules over the PR diff
env: { BASE_REF: ${{ github.base_ref }} }
run: combra-guard check-diff --base "origin/$BASE_REF"
Add guard as a required status check so a denied change can't merge.
Honest limits.
- The workflow file itself is not governed by the diff. A PR that rewrites
guard.ymlinto a green no-op is an inherent limit of any PR-triggered check. Protect it with branch protection + CODEOWNERS review on.github/workflows/. Theprotect-guard-configrule guardsguard.toml,guard.remote.toml,.claude/settings.json, and.githooks/**, but CODEOWNERS review of rule changes remains sound advice (a rule change is judged by the old rules, then takes effect after its own PR merges). - No wall until the first rule PR merges. A base branch that has never contained a
guard.tomlhas nothing to load; the rule-introducing PR turns the wall on. - No cross-file rule model. Rules are file-local; a rule that only makes sense across several files in one PR is outside the model.
- Octopus merges.
git merge-basereturns a single commit; a history with multiple merge-bases picks one. Correct by construction for an ordinary single-base PR — noted as a known limit.
check — the per-tool-call primitive
combra-guard check reads one Claude tool-call JSON on stdin
({"tool_name": "Write", "tool_input": {"file_path": ..., "content": ...}}) and exits non-zero on a
deny or any error. It is the low-level twin of the Action seatbelt; check-diff is the whole-PR wall
built on the same engine. Prefer check-diff for CI.
Read-guard
protected_read blocks Claude Read and Grep calls before the target enters model context.
Targets are expanded and matched as canonical absolute paths, so home paths, absolute paths,
repo-relative paths, and symlinks resolving outside the repo are covered without opening the file:
[[rules]]
id = "no-secret-reads"
type = "protected_read"
paths = ["~/.ssh/**", "~/.aws/credentials", ".env", ".env.*"]
cite = "secrets never enter model context"
deny_reason = "reading {path} is blocked"
At init and after every pull, these paths compile into Claude's native permissions.deny
entries such as Read(~/.ssh/**). Ownership is recorded under
permissions._combra_guard_managed; recompilation removes only those entries and preserves
hand-authored denies and unknown settings, using an atomic replace that never runs in a hook (where a
settings read-modify-write would race a live session).
The Bash matcher adds an opportunistic tripwire for literal spellings of protected paths and for
security find-generic-password / security find-internet-password.
Honest limits. Read-guard is accident-grade only — there is no turn, commit, or merge
backstop, because once content is read a later result scan cannot undo the leak. An adversarial or
prompt-injected agent bypasses it with indirection such as base64 < $(echo ~/.s*h/id_*); the real
backstop for reads is the OS sandbox (macOS Seatbelt, a container, or a devcontainer with a mount
allowlist). The Bash tripwire is literal-match, so variables, globs, and constructed paths bypass it.
MCP and WebFetch read channels are uncovered. A missed read is an irrevocable leak.
Write tripwire
An agent can dodge the Edit/Write matcher by rewriting a guarded file through the shell. The same
Bash PreToolUse hook opportunistically pre-denies the obvious cases: a literal path in a
plausible write position — a >/>> redirect target, sed -i … FILE, tee FILE, a cp/mv
destination, or dd of=FILE — that matches a protected_path rule. It fires only when the repo has
at least one protected_path rule (opt-in; the engine hardcodes no policy).
[[rules]]
id = "protect-migrations"
type = "protected_path"
paths = ["migrations/**"]
cite = "migrations are append-only; never rewrite a shipped migration"
deny_reason = "editing {path} is blocked"
echo hacked > migrations/001.sql # denied
sed -i 's/x/y/' migrations/001.sql # denied
python -c "open('migrations/001.sql','w')" # NOT caught — target buried in a Python string
The first two print:
combra-guard[protect-migrations]: editing migrations/001.sql is blocked — migrations are append-only; never rewrite a shipped migration (migrations/001.sql)
Honest limits. A seatbelt, not the wall. It does not parse arbitrary shell: a python -c write,
eval, a variable-obscured target, or a heredoc all bypass it. The redirect scan checks every
token, but the sed/tee/cp/mv/dd verb dispatch inspects only the first token — so those
verbs after a shell operator (x && sed -i … FILE, foo | tee FILE) are not caught. The
authoritative catch is the CI wall (sees the resulting diff) and the
turn gate (catches it by result).
Editor adapters
Cursor (preview — verified against docs, not yet live-tested)
Preview. Built to Cursor's documented hooks API (cursor.com/docs/hooks, config schema version 1), config emission unit-tested. Not yet run in a live Cursor session — the maintainers cannot run Cursor, and its hooks API is beta and has changed across releases. Please smoke-test it in a real Cursor session and open an issue with what you see. Do not rely on it as your only gate; the commit and CI gates are agent-agnostic.
combra-guard init --cursor is opt-in (a plain init never touches Cursor config). It compiles the
same guard.toml into .cursor/hooks.json:
| guard.toml rule | Cursor hook | Effect |
|---|---|---|
protected_read |
beforeReadFile |
denies the read before the file enters model context |
protected_path |
beforeShellExecution |
denies the obvious Bash writes to a protected path |
{
"version": 1,
"hooks": {
"beforeReadFile": [{ "command": "combra-guard cursor-read", "failClosed": true }],
"beforeShellExecution": [{ "command": "combra-guard cursor-shell", "failClosed": true }]
}
}
Both hooks carry "failClosed": true — Cursor's read/shell hooks default to fail-open, so a hook
crash on a matched file must block, not leak. Emission is atomic; hooks.json carries only
Cursor-documented keys, and ownership is tracked in a git-ignored sidecar
.cursor/.combra-guard-managed.json. Recompiling removes only our entries and preserves
hand-authored hooks and unknown keys. combra-guard cursor-read / cursor-shell read Cursor's stdin
JSON and answer in its response schema ({"permission": "deny", "user_message", "agent_message"}),
reusing the same read/write evaluators as the Claude hook — the two editors never diverge on a
decision.
Honest limits (Cursor).
- No pre-deny for file edits. Cursor exposes only
afterFileEdit(post-hoc), not a before-edit veto, soprotected_pathviolations written throughEditare not blocked in-editor; the commit and CI gates are the catch.beforeShellExecutioncovers only theBash-write shape, with the same limits as the write tripwire. - No content redaction. Cursor's
beforeReadFileresponse is permission-only (allow/deny). - No turn gate. Cursor's
stophook is not wired; the turn-scan backstop is Claude-only. - Beta-API drift: an unrecognized payload shape degrades to an allow (never a crash);
failClosedmakes a hook error block instead. - The community JSON schema at
unpkg.com/cursor-hooksstill lags and rejectsfailClosed— the adapter follows the official docs, not that schema.
Codex (preview — emission parse-verified against real Codex, not in-session-tested)
Preview. Built to OpenAI Codex's documented execpolicy
prefix_ruleformat. The emitted.rulesfile was parse-verified against the realcodex execpolicy checkengine (codex-cli 0.144.6): it loads, andsecurity find-generic-passwordresolves todecision: forbidden. Not yet verified in-session — whether a running Codex agent actually refuses the forbidden command outside its sandbox. Please smoke-test it in a real Codex session and open an issue. Codex execpolicy is experimental and may change. Do not rely on it as your only gate; the commit and CI gates are agent-agnostic.
Codex has no PreToolUse-equivalent and no edit pre-deny. Its native mechanisms are the
execpolicy — a command-prefix matcher over argv (forbidden > prompt > allow) governing
commands Codex runs outside its OS sandbox — and the OS sandbox itself. So combra-guard compiles
rather than clones a hook: combra-guard init --codex (opt-in) compiles guard.toml into a managed
block in .codex/rules/combra-guard.rules:
| guard.toml rule | execpolicy | Effect |
|---|---|---|
protected_read (any present) |
prefix_rule(pattern=["security","find-generic-password"], decision="forbidden") (+ find-internet-password) |
blocks reading a stored secret via the macOS keychain outside the sandbox |
# >>> combra-guard managed (do not edit inside this block) >>>
prefix_rule(
pattern = ["security", "find-generic-password"],
decision = "forbidden",
justification = "combra-guard: reading a stored secret via the OS keychain is blocked",
match = ["security find-generic-password"],
)
# ... find-internet-password ...
# <<< combra-guard managed <<<
The managed block is comment-fenced and emission is atomic, so a recompile rewrites only our block
and preserves any hand-authored Starlark around it. The .rules file is committed config — it is the
rulebook Codex reads — so it is not git-ignored.
Honest limits (Codex).
- Only the keychain secret-read tripwire compiles. execpolicy matches an argv prefix;
security find-*-passwordis an exact prefix, so it maps cleanly to aforbiddenrule. Path tripwires do not: a protected-path write (sed -i … guard.toml) or file read (cat ~/.ssh/id_rsa) puts the path in the last argv token, not a prefix, and there is no bounded set of write verbs. Those stay covered by the commit + CI walls. - No path/edit pre-deny, no diff rules in-session.
protected_path,forbidden_field, andforbidden_patterndo not run inside a Codex session — the commit and CI gates are Codex's real gate. - Outside-sandbox scope. execpolicy governs commands Codex runs outside its sandbox; the in-sandbox story is the OS sandbox's, not ours.
MCP advisory tool (preview — needs a real-MCP-client smoke test)
Preview & advisory.
combra-guard mcpis a consultation surface, not a gate. It lets an MCP-capable agent ask "would this action be denied by a local rule?" before doing it. Nothing forces an agent to call it, and ignoring the answer changes nothing — the Action, commit, turn, and CI gates catch the write regardless. Never count it as a security layer. Built to the MCP spec (2025-06-18), not yet tested against a live MCP host — please connect a real client and open an issue with what you see.
combra-guard mcp starts a minimal stdlib-only JSON-RPC-over-stdio MCP server (no SDK dependency).
Point any MCP client at it as a stdio server:
{ "mcpServers": { "combra-guard": { "command": "combra-guard", "args": ["mcp"] } } }
It exposes two tools, both reading the same local guard.toml the gates use (same evaluators — no
second matching implementation to drift):
| Tool | Input | Returns |
|---|---|---|
check_rule |
a hook-shaped action {tool_name, tool_input} (e.g. {"tool_name":"Read","tool_input":{"file_path":"secrets/x"}}, an Edit/Write, or a Bash {command}) |
{allowed, reason, rule_id} — the verdict the hook would give |
list_rules |
— | the loaded rules (id, type, paths) for discoverability |
Because it runs the same evaluate / evaluate_read / evaluate_bash the PreToolUse hook runs,
check_rule sees exactly what the Action gate would — including the opportunistic Bash write
tripwire. It does not run the diff-shaped commit/CI wall, so a rule that only fires on the full diff
won't surface here; that is by design (a fast self-check, not the wall). The AI rule-authoring
loop is a separate Combra-backend product, not part of this OSS package (see
docs/DESIGN.md §Phase 7).
Config reference
Rule types
forbidden_field— deny a newly-declared field offield_type(e.g.bool) inside a class named*<class_suffix>, underglobs. Anallow_sourceblock whitelists names already managed elsewhere. Existing fields are grandfathered — only new names fire.forbidden_pattern— deny an added line matching a regexpattern, underglobs. The{line}placeholder is the offending line.protected_path— guard files (paths) from write. Enforced at every write gate and by the Bash write tripwire. The{path}placeholder is the file.protected_read— block ClaudeRead/Greponpaths(canonical absolute) and compile to nativepermissions.deny. See read-guard.
Every rule takes an id, a cite (the policy rationale), and a deny_reason (the message, with the
placeholders above). A full annotated example ships as guard.example.toml.
Glob semantics
Write-rule glob matching (globs, paths) is Python fnmatch, evaluated against the repo-relative
path (no leading ./). protected_read is the exception: the target and absolute/home patterns are
canonical absolute paths, while relative patterns also match repo-relative. Mis-scoping a rule is a
security hole, so read these carefully:
*crosses/.fnmatch's*matches any run of characters including path separators, soapi/app/config/*.pymatchesapi/app/config/db.pyandapi/app/config/sub/nested.py. To pin a rule to one directory level, spell out the segments; there is no wildcard that stops at/.- No special
**operator.fnmatchhas no recursive-glob syntax —**is two adjacent*, which already spans directories..claude/**works only because plain*already crosses/. ?matches exactly one character;[seq]is a character class;[!seq]negates it.- Any-subtree-depth convenience. A pattern is also tried with a
*/prefix, so a bareconfig/*.pyfires whether the file sits at the repo root or under a subdir. - Case sensitivity follows the OS, via
os.path.normcase— case-sensitive on Linux and macOS, case-insensitive only on Windows. Don't rely on case to separate a matched from an unmatched path.
Worked example: ban bare feature-flag booleans
"No new bare bool toggle in a settings class." This guard.toml denies a newly-declared bool
field in a class *Settings under api/app/config/*.py unless the field name is already a key in an
ENV_PROFILES dict. Point the globs, class_suffix, and allow_source at your own layout:
[[rules]]
id = "no-env-flags"
type = "forbidden_field"
globs = ["api/app/config/*.py"]
field_type = "bool"
class_suffix = "Settings"
cite = "No feature flags: env holds secrets; toggles live in ENV_PROFILES, reviewed per-env."
deny_reason = "'{name}: bool' adds a bare feature-flag toggle. Register {name} in ENV_PROFILES, or make it a module constant."
[rules.allow_source]
# Allow set = the union of the INNER dict keys of `symbol = { outer: { field: value } }`.
kind = "nested_dict_value_keys"
path = "api/app/config/deployment.py"
symbol = "ENV_PROFILES"
Honest limits (read these)
The precise per-gate limits live in each gate's section; the summary:
- The Action, Turn, and Commit gates are seatbelts, not walls. A file rewritten through
Bash(sed,>,python -c) can dodge the Action write matcher;git commit --no-verifyskips the commit gate; the turn gate is Claude-only and blocks once. The write tripwire and turn gate narrow theBashhole, but the authoritative catch is the CI wall. - Read-guard is prevention-only and accident-grade. There is no result backstop for a read; a missed read is an irrevocable leak, and indirection defeats the tripwire. The OS sandbox is the real backstop.
forbidden_fieldcatches the config-field flag shape (a newbool-annotated field in aclass *Settings). It does not catch a flag smuggled asos.getenv("X")(add aforbidden_patternrule), a typed alias like PydanticStrictBool, or aBash-driven file rewrite.- Fail policy. The Action hook fails open on infra errors and closed on a detector error on a matched file. CI fails closed on any error — the wall never passes on error.
- Cursor / Codex / MCP are PREVIEW. The code ships, but Cursor and Codex are not run in a live session and MCP is not tested against a live host; each is documented against the vendor's spec. The MCP tool is advisory and enforces nothing.
Contributing
uv sync
uv run pytest # hermetic: throwaway git repos + hook-JSON fixtures, no network
uv run ruff check
uv run combra-guard selftest
uv build
Zero runtime dependencies is a hard invariant (pyproject.toml dependencies = []) — the hot path
is stdlib only (tomllib, ast, difflib, urllib, subprocess). Files stay under ~200 LOC and
one responsibility. When you add a rule type or an enforcement behavior, add a selftest golden case
and document what it does not catch. See docs/DESIGN.md for the invariants
(trusted-side rule loading, the guarded control surface) and the phased build history.
Licensed under Apache 2.0.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file combra_guard-0.1.0.tar.gz.
File metadata
- Download URL: combra_guard-0.1.0.tar.gz
- Upload date:
- Size: 137.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
aaa2e850f2aa6f917927cc3accb845fee0b989320999094ea6744af5de06a648
|
|
| MD5 |
27e6667a86deefa592f5b3b7fba9bce9
|
|
| BLAKE2b-256 |
bec655ba352ea526e46f4720b7b1eb6ce9f3e149a60c9d60169e86c91e24574f
|
File details
Details for the file combra_guard-0.1.0-py3-none-any.whl.
File metadata
- Download URL: combra_guard-0.1.0-py3-none-any.whl
- Upload date:
- Size: 82.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c5bc2b65e89ac337d62f604f892a5d891105bec33547a0d675b137c1333987fc
|
|
| MD5 |
5ce02885ad5d6b8b3d9bab260099aa53
|
|
| BLAKE2b-256 |
c6976d109c9001350c253b9f9a2bb8b9811d8cb3b0169cd8ae6c26860eec02ff
|