Skip to content

Policy schema

{
  "_doc": "optional free text",
  "bash_deny":    [{"pattern": "<regex>", "reason": "<text>"},
                   {"program": "kubectl", "verbs": ["delete", "drain"], "reason": "<text>"}],
  "bash_confirm": [{"pattern": "<regex>", "reason": "<text>", "when": "immediate"}],
  "path_confirm": [{"pattern": "<regex>", "reason": "<text>", "when": "commit"}],
  "mcp_allow":    ["<regex matched against the full tool name>"],
  "mcp_deny":     ["<regex matched against the full tool name>"],
  "providers": [{"name": "<text>", "command": "<program and arguments>", "questions": ["gate_bearing", "route_gate", "route_callers"], "timeout": 10}],
  "llm_classifier": {"enabled": false, "command": "<program and arguments, run without a shell>"},
  "snapshots": {"enabled": true, "max_per_session": 200, "exclude": [".env*", "*.pem", "*.key", "*secret*", "*credential*", ".gitvow/**", ".claude/**"]},
  "pricing": {"<model name or prefix>": {"input": 0, "output": 0, "cache_read": 0, "cache_write": 0}},
  "decisions": {"mode": "open", "session_scope": true, "authorities": [], "production_branches": ["main", "master", "production"], "rule_threshold": 3, "rule_decay_days": 90},
  "intent": {"from_prompt": true, "ask_when_missing": true},
  "outcomes": {"revert_window_days": 1, "rework_window_days": 7},
  "sync": {"automatic": false, "on_session_end": true, "on_push": true, "since": "90d"},
  "runtime": {"silent_route": "observe", "erroring_route": "commit", "error_rate_confirm": 0.01, "drift": "commit", "max_age_hours": 168}
}
Key Applies to Match Outcome
bash_deny Bash command text re.search on pattern, or on the regex built from program and verbs deny
bash_confirm Bash command text as above confirm; when defaults to immediate
path_confirm file path of Edit, Write, MultiEdit, NotebookEdit re.search confirm; when defaults to commit
mcp_deny MCP tool name re.fullmatch deny
mcp_allow MCP tool name re.fullmatch; if the list is non-empty, non-matching tools → confirm
providers Edit/Write paths and route literals in Edit text each provider asked its declared questions; see Provider protocol yes → confirm at commit with evidence; failure → confirm immediately
pricing note, ledger, report, digest not a gate rule; USD per million tokens by model, overriding the built-in table
snapshots after each agent edit not a gate rule; controls working-tree snapshots, see Snapshots
llm_classifier anything not decided above command prints ALLOW / CONFIRM reason / DENY reason as printed, immediately; failure → confirm
decisions the card and the git hooks not a gate rule; mode open (default: the agent's commit is stopped once with the card, then goes through with Gitvow-Open for every unanswered finding; a person's commit goes through the same way) or strict (agent and person alike refused until every finding is decided); authorities names whose decisions count and who alone may accept a proposed rule, matched against committer email, its local part or name; production_branches where scoped decisions are reopened by the report; rule_threshold consistent unscoped answers by authorities before a finding is proposed as a rule, and the number of answers dated after a rejection before it is proposed again; rule_decay_days days without a new confirmation before a rule lapses; session_scope (default true) records an immediate confirm as a finding and lets one answer hold for the session, false asks every time and records nothing
intent session start and the first message not a gate rule; from_prompt (default true) records the first line of the session's first message as the intent in Claude Code, redacted, at most 200 characters, source=prompt on the trailer; ask_when_missing (default true) tells the agent at session start to ask once, in one line, when the first message did not say what the task is for. See State the intent
outcomes gitvow outcomes not a gate rule; revert_window_days (default 1, 1 to 365): a decision-bearing commit reverted within this many days of landing is graded as a decision that did not hold; a later revert is recorded and does not change the grade. rework_window_days (default 7): an accepted change whose added lines someone else rewrites within this many days is graded reworked. See Did the decision hold?
sync session end and the agent's git push not a gate rule; automatic (default false) starts gitvow sync in the background at those moments when a sink is configured in .gitvow/export.local.json or ~/.gitvow/sinks.json; the hook never waits for it; the child logs to ~/.gitvow/sync.log and an unreachable store leaves the bundle in the outbox. on_session_end and on_push (both default true) choose the moments; since (default 90d) the window carried. Sessions are never synced automatically.
runtime edits to files carrying a route the store has runtime facts for two findings from the brief's facts[], read from the local cache: silent_route (default observe) when the route was measured at zero requests over the window; erroring_route (default commit) when its 5xx share is at or above error_rate_confirm (default 0.01). drift (default commit): an edit to a manifest declaring a service the cluster is not running as declared, from the blueprint's observation. Tiers observe, commit, immediate or off. max_age_hours (default 168): an older fact is shown, not acted on. Each finding and its decision carry the observation's ref. See Runtime facts at the gate

program and verbs

A command rule may name a program (or a list of them) and the verbs that make it consequential, instead of a hand-written regex. gitvow builds the regex and allows options between the two: kubectl --context prod delete pod x is a kubectl delete. A hand-written \bkubectl\s+delete is not, and until 0.17.1 the default policy was written that way; replaying three engineers' real sessions found 163 cluster mutations that had walked past it on a --context flag. Each verb is a regex fragment, so multi-word verbs are written pr\s+merge. The finding a program rule raises is run <program> (<reason>), so the program name is what precedent attaches to. Write new command rules in this form; keep pattern for shapes it cannot express, such as the database-write rule, which needs a client program and a statement anywhere after it.

Since 0.26.1 a program rule matches only where the program is the word that runs: the line is split into its simple commands at ;, &&, ||, | and newlines, heredoc bodies are dropped, VAR=x assignments and sudo, env, time, timeout N, nohup, xargs are stepped over, and a sh -c "…" argument is read as a command line of its own. A program named inside another program's quoted argument (gh issue create --body "run kubectl exec …"), in a commit message, or in a heredoc is text, not a command, and does not fire the rule. Before 0.26.1 it did, and an issue whose body described a runbook was stopped as a cluster mutation. pattern rules are unchanged: a plain regex over the whole line, which is what you want for the database-write shape and what you must remember when you write one. A line that cannot be tokenised (an unbalanced quote) falls back to the whole-line regex, failing toward asking.

target: what the command reaches

Text cannot tell kubectl apply against a kind cluster from the same string against production. The environment can. A program rule may add target, a regex on the command's reach, which gitvow reads before the call runs and only once the rule has matched:

program target string read from
kubectl, helm context=<ctx> namespace=<ns> --context / --kube-context, else the kubeconfig's current-context (--kubeconfig, $KUBECONFIG, ~/.kube/config); -n/--namespace
git push remote=<url> branch=<name> the named remote or origin, resolved with git remote get-url; the refspec's destination or the current branch
terraform, tofu workspace=<name> dir=<dir> -chdir and .terraform/environment under it, else default

Rules are tried in order, so write the specific tier first and the catch-all after it. The default policy does exactly that for cluster mutations and helm releases: target: "(?i)prod|live" asks immediately, the same verbs without a target are observe. A target that cannot be read matches every target rule: an action whose reach is unknown is asked about, never waved through. The target joins the finding's subject (run kubectl (cluster mutation in production) @ context=prod-in), so standing and earned rules accrue per place reached. target is only for rules written as program and verbs; a pattern rule has nothing to attach it to.

decisions.indirect_commands

eval, source, a variable in program position ($KUBECTL delete pod x) and a shell running a script file (bash deploy.sh) name an indirection, not a program, and no rule on text can read what runs. Until 0.27 they fell through to allow. They are now a tier of their own: observe by default (a finding on the commit, nobody asked), or commit, immediate or allow. Reads are still not rules; this is about the gate saying honestly what it could not see, and letting the record show how often that happens before anyone decides whether it deserves a question.

decisions.commit_window_seconds

When the gate sees the agent run git commit, it marks the repository's session state, and the git hooks write the session trailers on the commit that follows while that mark is fresh. The mark exists so that a person's own commit, minutes later at a terminal, is not mistaken for the agent's. How long "fresh" is was five minutes; it is now this setting, default 1800, because a harness that runs tool calls in parallel can queue the commit behind a long test run, and a commit that lands past the window carries no trailer and no note. When that happens anyway, PostToolUse says so to the agent, names the commit and the gap, and logs commit_without_trailer, so the hole is visible rather than silent.

when

"when": "immediate" refuses the call and asks now; since 0.18 the finding is recorded with a number and a person's answer holds for the session (see decisions.session_scope). "when": "commit" records a finding and lets the call run; the finding is put to a person when the agent commits, see Decisions. "when": "observe" records the finding and lets the call run, and nobody is ever asked: it rides on the commit as Gitvow-Observed, is counted by the digest, and is never precedent. Deny rules have no when; a denial is always immediate. The default policy keeps edits to gitvow's own policy and hooks immediate, so a loosened policy can never take effect before a person has seen it.

Evaluation order: deny, confirm, MCP lists, providers, classifier, allow. Missing keys are treated as empty lists. Every pattern is compiled on load; a compile error or unreadable file makes the gate fail closed.

No key grants a rule. Reaching rule_threshold proposes one; a person named under decisions.authorities accepts it with gitvow rules accept, and that acceptance is a trailer on a commit rather than a line in this file, so no edit here can give a repository rules nobody agreed to. A scoped answer is an exception and never counts towards the threshold. See Decisions.

Lookup order: <repo>/.gitvow/policy.json, ~/.gitvow/policy.json, package default.

The organisation pack

A store may publish a pack for a repository: rules and settings a named person accepted for the whole organisation. gitvow sync copies it from a git sink (packs/<source>.json) to ~/.gitvow/cache/packs/<source>.json, and the policy loader applies it as a layer under the repository's own policy:

  • pack rules (bash_deny, bash_confirm, path_confirm only) are added, each tagged with the pack it came from; a pattern the repository already has is not added twice, so the repository's own tier for it stands;
  • pack settings may tighten decisions.mode (open → strict), turn session_scope off, or raise rule_threshold; a pack can never loosen any of them;
  • a pack must carry expires; past that date it is not applied. A pack that is missing, expired or malformed changes nothing and gitvow pack says why.

The cache is advisory: nothing in it can stop the gate from loading its own policy. The pack is never written into .gitvow/policy.json, so gitvow policy accept and a pull request to the policy file see the repository's own rules only. gitvow pack shows what is in force; session start hands the same to the agent as context.