---
name: synthesis-git-hooks
description: "Deterministic pre-commit policy for the synthesis-engineering workflow. Classifies each repo by publication surface (personal / public-surface / strict) from its push remotes, applies a tiered pattern set: Tier 0 credentials always; Tier 1 financial / HR / confidentiality / client names in strict and public-surface repos — public-surface minus only the disclosure ledger's published-precedent names. YAML-driven policy lives in ~/.synthesis/git-hook-config.yaml. Use when asked to: install git hooks, configure pre-commit policy, prevent credential leaks, prevent confidential-name leaks, allow published bio names on my own sites, disclosure ledger enforcement, set up the synthesis-engineering enforcement layer."
license: "Apache-2.0"
depends_on: []
metadata:
  author: "Rajiv Pant"
  version: "2.3.0"
  source_repo: "github.com/synthesisengineering/synthesis-skills"
  source_type: "public"
---

# 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 small (one bash script + one Python sidecar). The policy is data — a YAML file at `~/.synthesis/git-hook-config.yaml` that anyone adopting synthesis engineering fills in with their own personal-remote patterns, client names, and internal URLs.

## 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.

## 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:

1. **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=1` sentinel 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.
2. **Zero third-party dependencies.** v1 required PyYAML; which `python3` won 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.
3. **Commit-message scanning (v2.1.0).** A sibling `commit-msg` hook 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_message` config flag); fail-closed semantics are identical to pre-commit.
4. **`--doctor` self-check.** `python3 ~/.synthesis/git-hooks/_load_config.py --doctor` verifies the whole chain: config parses, every pattern compiles under both Python `re` and `grep -E`, `core.hooksPath` is 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:

1. **`strict`** — ANY push remote matches `strict_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.
2. **`public-surface`** — EVERY push remote matches
   `public_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.
3. **`personal`** — EVERY push remote matches `personal_remote_patterns`:
   content only the user reads. Tier 0 only.

Ledger allowances come from `disclosure_ledger` (see the
[`synthesis-disclosure-policy`](../synthesis-disclosure-policy/SKILL.md)
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-verify` bypasses
- 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

```bash
# 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.
```

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

```bash
cd <repo>
~/.synthesis/git-hooks/_load_config.py --classify
# → personal | strict
```

Inspect the underlying remotes:

```bash
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 `exec`s 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 -v` is 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)** | **Credential + exposure-sensitive pattern check at 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
```

## 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-shortcuts` skill.

## 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.
