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:
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_confirmonly) 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), turnsession_scopeoff, or raiserule_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 andgitvow packsays why.
.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.