Synthesis Git Hooks
A YAML-driven pre-commit policy engine. Part of the synthesis-engineering operational layer — deterministic enforcement that catches credential leaks and exposure-sensitive content at the commit boundary, before the diff persists.
The engine is a Bash boundary plus standard-library Python sidecars. The policy
is data — a YAML file at ~/.synthesis/git-hook-config.yaml that anyone
adopting synthesis engineering fills in with personal-remote patterns, client
names, internal URLs, and optionally a coordination-board path.
v2.6.0 — Cached pattern validation
The sidecar validates the configured pattern set once per config digest and
grep identity and reuses that result on later commits instead of re-running
every pattern through grep -E at each commit boundary; a changed config or
grep invalidates the cache. Install writes the cache directory with the
engine. Refusals and surface classes are unchanged.
v2.5.0 — Required repo-local delegate, fail closed
Global core.hooksPath makes this chain the only path to a repository's own
.githooks/pre-commit; an absent or mode-644 delegate used to be skipped
silently, so a repository's own commit guards reported success while running
nothing. Declaring .githooks/required opts in — declared means the marker
is present in the working tree or listed in the index; content is ignored.
With the declaration, a missing, non-regular, or non-executable delegate (a
symlink is judged by its target) blocks the commit and names the remedy
(create .githooks/pre-commit or chmod +x .githooks/pre-commit); without
it nothing changes. Withdrawal is a staged git rm .githooks/required in a
reviewed commit; an unstaged rm leaves the index entry, and the
declaration, standing, unless the commit itself stages the removal
(git commit -a, or a partial commit naming .githooks/required). The
doctor's delegate-required control applies the same rule and reports the
same verdict.
v2.4.0 — Coordination claims at the commit boundary
When coordination_board is configured, pre-commit invokes
synthesis-project-management's check-staged before content scanning or a
repo-local hook. It refuses unless the selected active session owns the exact
worktree and branch and every staged source/destination path is covered by the
session's claims. SYNTHESIS_COORDINATION_SESSION supplies the committing
session when the active-project pointer does not. A deliberate outside-claim
exception requires SYNTHESIS_COORDINATION_OVERRIDE_REASON; the checker
atomically records it on the board before the commit proceeds. Missing runtime,
board, lease refresh, selector, or index evidence fails closed.
The hook accepts only passed-inside-claim and recorded-override, requires the
outcome and outside-path list to match their hash-bound receipt fields, and
revalidates the board and index before continuing. A coordination refusal is
reported separately from content-policy-engine unavailability.
Repositories remain usable when coordination is not adopted: omitting
coordination_board does not block, and each hook invocation explicitly says
that this control is absent. The credential and exposure scanners still run.
v2.3.0 — Portable drift-source resolution
v2.3.0 (2026-08-03) removes the doctor's hardcoded personal checkout path.
The drift check's source now resolves portably: $SYNTHESIS_GIT_HOOKS_SOURCE
when set (authoritative — an empty value skips the check deliberately; an
invalid value is a doctor problem, fail closed), else the running script's
own directory when it is not itself an installed engine copy (repo
checkouts, worktrees, client plugin caches), else the documented locations
the ecosystem's own installers create (direct-copy skill installs, the
shared installer's cached clone). A source must carry all three engine
files to qualify, and the doctor names the resolved source in its output.
The v2.4.0 installer also persists its absolute source directory beside the
installed engine. Later direct doctor runs use that pointer before documented
fallback locations; an invalid or missing pointed source is a doctor failure.
v2.1.2 — Exact-copy migration calibration
v2.1.2 (2026-07-30) recognizes exact copies from already-committed files before
scanning added lines. Canonical instruction migrations such as
CLAUDE.md to AGENTS.md therefore scan the new adapter and any actual edits
without treating the unchanged historical instruction body as a fresh leak.
Genuinely new sensitive lines still block, covered by a paired regression.
v2.1.1 — Native dual-runtime setup
v2.1.1 (2026-07-29) makes the native synthesis plugin the primary skill setup
for Codex and Claude Code. The enforcement runtime remains agent-neutral under
~/.synthesis/git-hooks/; both clients invoke and diagnose the same installed
engine.
v2.0.0 — fail closed, zero dependencies, self-diagnosing
Three design guarantees, added after a field incident in which the v1 engine silently passed commits unscanned when its Python dependency was missing in the invoking environment:
- Fail closed. If the policy engine cannot run — missing config, unparsable config, sidecar crash, invalid pattern, missing interpreter — the commit is blocked with a loud diagnostic, never passed unscanned. The engine verifies a
SYNTHESIS_SIDECAR_OK=1sentinel emitted as the sidecar's final line, so even a partial failure blocks. A protective control that fails open is worse than no control, because it manufactures false confidence. - Zero third-party dependencies. v1 required PyYAML; which
python3won PATH resolution therefore silently determined whether protection ran at all (a machine can carry several interpreters — an OS-bundled one, a package-manager one, a python.org one — with different site-packages). v2 vendors a strict YAML-subset parser using only the standard library: any python3 ≥ 3.6, in any environment, yields byte-identical policy. The supported subset is: comments, nested mappings by indentation, quoted/bare string lists, and scalar values — anything outside it (tabs, flow style, anchors, block scalars) is a hard parse error that blocks commits rather than a guess. - Commit-message scanning (v2.1.0). A sibling
commit-msghook scans the commit message itself against the strict-class pattern set — closing the channel pre-commit cannot cover (git gives pre-commit no reliable access to the new message). A history audit showed the only real public-repo confidentiality violations had arrived through commit messages, the one channel the engine never scanned; hygiene-by-discipline demonstrably fails. Personal-class repos skip message scanning by design (check_commit_messageconfig flag); fail-closed semantics are identical to pre-commit. --doctorself-check.python3 ~/.synthesis/git-hooks/_load_config.py --doctorverifies the whole chain: config parses, every pattern compiles under both Pythonreandgrep -E,core.hooksPathis wired, the installed engine matches the skill source (drift detection — installed copies that were hot-fixed but never synced back are themselves a protection failure), and the cwd repo's classification plus chained repo-local hook. Wire it into a daily ritual and into new-machine bootstrap; a protection layer nobody monitors is a protection layer that is quietly broken.
What this enforces
| Tier | Patterns | When applied |
|---|---|---|
| Tier 0 — credentials | API keys (AWS, OpenAI, Anthropic, Google, GitHub, GitLab, Slack), private key markers (RSA, OpenSSH, EC, PGP) | Every repo. Credentials don't belong in git regardless of who reads. |
| Tier 1 — exposure-sensitive | Financial, HR/employment, confidentiality markers, confidential client/company names, private skill names, internal URLs | Skip when the repo classifies as personal. Run in strict and public-surface repos — in public-surface, minus only the exact name patterns the disclosure ledger records as published precedent. |
Classification is derived from git remote -v on every commit and follows
the PUBLICATION SURFACE, strict-first:
strict— ANY push remote matchesstrict_repo_patterns(public OSS repos pinned strict even under a personal org), the remote list is empty, or nothing else matches. Full Tier 1, commit-message scan on.public-surface— EVERY push remote matchespublic_surface_patterns: sites and other surfaces whose content the user personally authors and publishes, regardless of repository visibility. Full Tier 1 minus ledger allowances; commit-message scan on.personal— EVERY push remote matchespersonal_remote_patterns: content only the user reads. Tier 0 only.
Ledger allowances come from disclosure_ledger (see the
synthesis-disclosure-policy
skill): each ledger entity carries evidence citations and hook_patterns
strings that must textually equal tier_1_strict_only entries. A
configured ledger that is missing or unparsable fails closed — commits on
public-surface repos block until it is fixed. No per-repo flag file, no
static declaration, no drift potential: the remote configuration plus the
ledger IS the security profile.
When to apply
- Setting up a new workstation as part of the synthesis-engineering install
- Auditing a system where false positives are driving repeated
--no-verifybypasses - Adopting synthesis engineering as a team (the policy schema is per-user; the engine is shared)
When NOT to apply
- One-off scripts or throwaway repos where policy infrastructure is overkill
- Environments where you genuinely need to commit credentials (very rare; almost always indicates a missing secrets store)
- CI/CD pipelines that run their own credential-leak scanners (the pre-commit is a developer-side layer; CI/CD-side scanning is a complementary, not redundant, control)
Install
# 1. Install the native plugin in the client you use
codex plugin marketplace add synthesisengineering/synthesis-skills
codex plugin add synthesis-skills@synthesis-engineering
# Claude Code equivalent
claude plugin marketplace add synthesisengineering/synthesis-skills
claude plugin install synthesis-skills@synthesis-engineering
# 2. Run the install script — copies the engine to ~/.synthesis/git-hooks/,
# sets git's core.hooksPath, and seeds an initial config from the template.
<synthesis-git-hooks-root>/scripts/install.sh
# 3. Edit ~/.synthesis/git-hook-config.yaml with YOUR personal-remote patterns
# (your GitHub user/org), confidential names, internal URLs.
# 4. To enforce lease-backed source-area claims, enable the optional boundary:
# coordination_board: '~/.synthesis/coordination/active-sessions.md'
# Then export SYNTHESIS_COORDINATION_SESSION=<your board selector> in commit
# processes that do not own the active-project pointer.
After install, every git commit on the workstation runs the policy. No per-repo configuration needed; classification is automatic from each repo's push remotes.
Verifying classification for any repo
cd <repo>
~/.synthesis/git-hooks/_load_config.py --classify
# → personal | strict
Inspect the underlying remotes:
git remote -v | awk '/\(push\)/ {print $2}'
Override path / bypass
| Need | Mechanism |
|---|---|
| Use a different config file for one invocation | SYNTHESIS_GIT_HOOK_CONFIG=/path/to/custom.yaml git commit ... |
| Skip the hook once (last resort) | git commit --no-verify |
| Add a legitimate match to the allowlist | Add a regex to allowlist_lines in the config |
| Add a new personal org (sole-owner repos there) | Add a regex to personal_remote_patterns in the config |
The --no-verify escape valve is genuine, but each use weakens the discipline. If a pattern fires repeatedly as a false positive, fix the underlying signal: rename the variable, extend allowlist_lines, or — if the repo really is sole-owner — add the right personal_remote_patterns entry.
Repo-local hooks are additive, not superseded
If a repo has its own .githooks/pre-commit (version-controlled, executable), this engine chains to it — runs its own Tier-0/Tier-1 pass first, then execs the repo-local hook. It does not replace or subsume it.
This matters because it's easy to assume the opposite: "the global hook already covers confidentiality, so the repo-local one is redundant — delete it." That assumption is wrong and removes protection rather than deduplicating it. A repo-local hook typically exists because the repo needs a check the global config can't express safely — for example, a repo whose whole purpose is documenting a specific client relationship needs personal-class handling (so the client's own name isn't flagged as a leak) while still blocking a different category the global patterns don't cover, like engagement financials or a partner's personnel names. Verify what a repo-local hook actually checks before assuming it's covered elsewhere, and don't delete it as part of unrelated cleanup.
Why auto-derive instead of per-repo flag files
The classification could have been a per-repo flag file (.githooks/sole-owner or similar). It isn't. Reasons:
- Single source of truth. A flag file is a SHADOW of the real security profile (the remotes). Two sources of truth drift; one doesn't.
- No silent erosion. If a repo's profile changes (a new collaborator's remote is added), auto-detect tightens immediately. A flag file would stay relaxed even after reality changed.
- Zero per-repo ritual. A new sole-owner repo classifies correctly on its first commit. No "did I add the flag file?" checklist.
- Self-documenting.
git remote -vis one command; the classification logic is one regex match against URLs.
Counter-analogy from CSP allowlists (which ARE static for adversarial reasons): doesn't apply here. The user isn't adversarial against themselves, and no third party can manipulate the remote set.
Reciprocal layers in the synthesis-engineering enforcement stack
This skill is one of four deterministic-enforcement layers. Each runs at a different point in the agentic workflow:
| Layer | What it enforces | Trigger |
|---|---|---|
synthesis-anti-shortcuts |
Costume-vocabulary detection in agent outputs | Stop hook + PreToolUse hook |
| (agent-rules sync) | Single source of truth for CLAUDE.md / AGENTS.md / ~/.codex/AGENTS.md | PostToolUse hook on edits |
synthesis-git-hooks (this skill) |
Coordination-claim enforcement plus credential and exposure-sensitive checks at the commit boundary | pre-commit |
synthesis-repo-guard |
Uncommitted changes + unpushed commits | Session-end skill |
The discipline isn't a prompt the agent has to remember — it's a runtime check the agent can't route around. This is the differentiator from vibe coding / agentic coding / spec-driven development: methodology becomes runtime infrastructure, not a Markdown file the agent may or may not consult.
Files in this skill
synthesis-git-hooks/
├── SKILL.md # this file
├── scripts/
│ ├── pre-commit # bash engine — wired via core.hooksPath
│ ├── _load_config.py # YAML→regex sidecar
│ ├── install.sh # idempotent installer
│ └── git-hook-config.example.yaml # template config (adopters customize)
└── references/
├── threat-model.md # why two tiers; what each tier protects
├── tier-classification.md # how `git remote -v` becomes the class
└── per-repo-overrides.md # delegation to repo-local .githooks
The installer also copies coordination.py, claim_scope.py, coordination_schema.py,
pointer_lock.py, peer_addressing.py, and the versioned session-word asset from the declared
synthesis-project-management dependency into the shared runtime. Their
canonical source remains in that owning skill; the git-hooks doctor compares
the installed copies with those source paths and reports drift. source-path
records the install-time source directory so direct installed-doctor runs can
repeat that comparison without an environment override.
Companion artifacts
- Design rationale (full five-mode analysis) will be published in the synthesis-engineering blog series.
- Operational lesson on the recurring infrastructure-design shortcut pattern that prompted this redesign — included as a reference in the
synthesis-anti-shortcutsskill.
License
Apache-2.0. Engine and scripts may be used, modified, and redistributed under the terms of the LICENSE-APACHE file at the root of the synthesis-skills repository.