Skip to main content

Ground rules

  • No runtime dependencies. Anything that needs a third-party package belongs in an optional extra or outside this project.
  • Every behaviour has a test. Redaction patterns get positive and negative cases. Policy rules get a deny, a confirm and an allow case. Anything touching git runs against a real temporary repository.
  • Docs first. Change docs/ in the same pull request as the code, and change the docs before the code when the behaviour is user-visible.
  • Fail closed. If a hook cannot decide, it blocks.
  • Never widen what enters git. Notes hold structure and redacted text, never tool output.
  • Our vocabulary: session, step, session note, ledger, gate, policy, self-check. Do not import terminology from other products.

Development

The real-harness check

The unit suite drives hook functions with synthetic payloads in temporary repositories. It cannot see what a real agent does: dispatch tool calls in parallel, run a commit minutes after the gate saw it, reach into another checkout, merge two settings files and run every hook twice. Seven of the first eight defects found in real use lived there. scripts/e2e_real.py drives Claude Code itself:
It builds two sandbox repositories with bare remotes, installs this checkout’s gitvow in both, writes the hooks into an isolated CLAUDE_CONFIG_DIR, and asks the agent to edit and commit in both (one through git -C) and push to a protected branch. Then it reads the record and asserts: trailers on both commits, notes, the production-values finding recorded in the repository it belongs to, the card shown once, the push asked about and not made. --keep leaves the sandbox for inspection; --json prints the summary as data. scripts/e2e_agents.py does the same for the agents that are not Claude Code, through their own hook contracts: OpenCode on one of its free models, no sign-in, and Gemini CLI with the operator’s key in ~/.gemini/.env (copied into the throwaway home, never read or printed). Each gets a sandbox repository with gitvow from this checkout installed at repository scope, two sessions (an edit and a force push the gate must refuse; a gated file and a commit the card must stop once), and the same assertions on the record, plus one per agent: OpenCode’s first message became the intent, and under Gemini gitvow wrote the trailers itself because Gemini disables git hooks. --agent opencode or --agent gemini runs one; an agent that is not installed, Gemini without a key, or a provider’s free quota spent for the day is reported as skipped rather than failed. The nightly wrapper runs both scripts and writes latest.json with all three agents. It needs claude on PATH and a credential: CLAUDE_CODE_OAUTH_TOKEN (from claude setup-token), or on macOS the keychain item Claude Code itself writes, which is read and never printed. Run it before a release and after any change to hooks/, install.py or policy.py; a nightly run on a machine that is logged in is the intended home. For a nightly run on macOS with launchd, keep the wrapper and its clone outside ~/Documents: a launch agent cannot read ~/Documents without Full Disk Access, and the first attempt failed with “Operation not permitted” before it ran anything. The wrapper we use clones main under ~/.gitvow/e2e/checkout, refreshes it each night, installs it into its own virtualenv and runs the check against that, which is also the right thing to test: main as pushed, not a working copy. Results land in ~/.gitvow/e2e/<date>.json, latest.json and history.log.

Pull requests

  • One change per PR. Describe the behaviour change and the user-visible effect.
  • Add a line to CHANGELOG.md under Unreleased.
  • CI must be green: lint, tests on 3.9–3.12 across Linux and macOS, bandit, pip-audit, CodeQL.

Releasing

Bump version in pyproject.toml, move the Unreleased section of CHANGELOG.md under the new version with today’s date, commit, then git tag vX.Y.Z && git push origin vX.Y.Z. The release workflow builds, checks that the tag matches the version, publishes to PyPI through trusted publishing (no tokens stored anywhere), creates the GitHub release with the changelog section as notes, and moves the vMAJOR.MINOR tag that workflows reference as wirevow/gitvow@vX.Y.

Code of conduct

Be kind, be specific, assume good intent. Report conduct issues to the maintainers listed in pyproject.toml.