SuperAPI AI Directory

synthesis-git-hooks

Deployment & operations

View repository

Updated 10-22 16:14

About this skill

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:

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

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