Skip to main content

combra-guard

CI License: Apache 2.0

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.toml at the repo root (including an active protect-guard-config rule that guards guard.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, and Bash PreToolUse hooks plus the turn-gate UserPromptSubmit/Stop/SubagentStop hooks to .claude/settings.json, never clobbering hooks you already have;
  • compiles protected_read paths into Claude's native permissions.deny;
  • installs the git commit gate and appends guard.remote.toml to .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 UserPromptSubmit it snapshots the turn-start state — the trusted rules plus the content of every guarded file (including git-ignored control files like guard.remote.toml, and any allow_source a rule references) — into <git-dir>/combra-guard/turn-<session_id>.json.
  • On Stop / SubagentStop it 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/hooks isn't shared by a clone; .githooks/ is tracked, so cloning + one init arms every checkout.
  • Never-clobber. If core.hooksPath already points elsewhere, or a foreign pre-commit already lives in .githooks/, init prints 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 worktree guard.toml. The first commit that introduces the control files (guard.toml, .githooks/**, .claude/settings.json) must bootstrap with git 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-guard off PATH, the hook's command not found exits non-zero, so every git commit is blocked (not a silent no-op) until you install it — or git commit --no-verify in the meantime.
  • Linked worktrees. core.hooksPath is relative, resolved per worktree. A worktree on a branch that predates the gate has no .githooks/, so git runs no hook there. Re-run init there to arm it.
  • Symlinked .githooks. If .githooks is a symlink, the hook write follows it to its target; init does 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. --base diffs merge-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.toml is still judged by the pre-PR rules — it cannot disable the wall from inside the same PR. An allow_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.toml is git-ignored, so CI re-runs combra-guard pull to 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_path file 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.yml into a green no-op is an inherent limit of any PR-triggered check. Protect it with branch protection + CODEOWNERS review on .github/workflows/. The protect-guard-config rule guards guard.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.toml has 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-base returns 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, so protected_path violations written through Edit are not blocked in-editor; the commit and CI gates are the catch. beforeShellExecution covers only the Bash-write shape, with the same limits as the write tripwire.
  • No content redaction. Cursor's beforeReadFile response is permission-only (allow/deny).
  • No turn gate. Cursor's stop hook is not wired; the turn-scan backstop is Claude-only.
  • Beta-API drift: an unrecognized payload shape degrades to an allow (never a crash); failClosed makes a hook error block instead.
  • The community JSON schema at unpkg.com/cursor-hooks still lags and rejects failClosed — 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_rule format. The emitted .rules file was parse-verified against the real codex execpolicy check engine (codex-cli 0.144.6): it loads, and security find-generic-password resolves to decision: 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-*-password is an exact prefix, so it maps cleanly to a forbidden rule. 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, and forbidden_pattern do 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 mcp is 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 of field_type (e.g. bool) inside a class named *<class_suffix>, under globs. An allow_source block whitelists names already managed elsewhere. Existing fields are grandfathered — only new names fire.
  • forbidden_pattern — deny an added line matching a regex pattern, under globs. 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 Claude Read/Grep on paths (canonical absolute) and compile to native permissions.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, so api/app/config/*.py matches api/app/config/db.py and api/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. fnmatch has 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 bare config/*.py fires 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-verify skips the commit gate; the turn gate is Claude-only and blocks once. The write tripwire and turn gate narrow the Bash hole, 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_field catches the config-field flag shape (a new bool-annotated field in a class *Settings). It does not catch a flag smuggled as os.getenv("X") (add a forbidden_pattern rule), a typed alias like Pydantic StrictBool, or a Bash-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

combra_guard-0.1.0.tar.gz (137.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

combra_guard-0.1.0-py3-none-any.whl (82.1 kB view details)

Uploaded Python 3

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

Hashes for combra_guard-0.1.0.tar.gz
Algorithm Hash digest
SHA256 aaa2e850f2aa6f917927cc3accb845fee0b989320999094ea6744af5de06a648
MD5 27e6667a86deefa592f5b3b7fba9bce9
BLAKE2b-256 bec655ba352ea526e46f4720b7b1eb6ce9f3e149a60c9d60169e86c91e24574f

See more details on using hashes here.

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

Hashes for combra_guard-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 c5bc2b65e89ac337d62f604f892a5d891105bec33547a0d675b137c1333987fc
MD5 5ce02885ad5d6b8b3d9bab260099aa53
BLAKE2b-256 c6976d109c9001350c253b9f9a2bb8b9811d8cb3b0169cd8ae6c26860eec02ff

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page