git-customs

command module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 12, 2026 License: MIT Imports: 1 Imported by: 0

README

git-customs

A client-side, cross-host pre-push gate that catches PII and secrets — using an engine you choose — before anything reaches a public remote.

git-customs is a router + pipe, not a scanner: it decides which repos to inspect (only public-bound pushes, on any host — GitHub, GitLab, Gitea, self-hosted), builds the diff, and pipes it to a command you configure. That command can be an LLM (claude -p "…") — which catches free-form PII that regex scanners miss — or a deterministic tool like gitleaks. git-customs itself stays dumb and engine-agnostic.

Pushing to a private repo? It skips silently. Publishing a new public repo? It scans the whole tree once. Everything in between just works.

Why it exists (and what it is not)

It complements — does not replace — GitHub push protection. Push protection is excellent and you should keep it on. But it is server-side (your data already reached GitHub), GitHub-only, secrets-only (no PII), and limited to known token types. git-customs covers the seams push protection leaves:

  • Client-side / pre-upload — a flagged diff never leaves your machine, not even to a server.
  • Cross-host — works on self-hosted GitLab/Gitea and anywhere else, where push protection doesn't exist.
  • PII, not just secrets — point it at an LLM and it catches names, emails, addresses, and IDs that pattern matchers don't.
  • Bring your own engine — claude, gitleaks, or any executable; not tied to one vendor or service.

It is not org enforcement: like any local hook it is bypassable (--no-verify) and isn't team-distributed. Treat it as a personal safety net, with push protection as the server-side backstop.

Install

Get the binary with Homebrew:

brew install jackchuka/tap/git-customs

Or with Go:

go install github.com/jackchuka/git-customs@latest

Then install the hook:

git customs install

install writes a pre-push hook to ~/.config/git-customs/hooks/ and points your global git config core.hooksPath at it, so it runs in every repo on the machine. A starter ~/.config/git-customs/config.toml is created if absent.

Because the binary is named git-customs and sits on your PATH, git exposes it as a native subcommand: git customs install, git customs doctor, etc.

How a push flows

  1. pre-push fires → git-customs reads the outgoing ref updates.
  2. It resolves whether this repo should be inspected (see mapping below). Skipped → the push proceeds untouched.
  3. If inspected, it builds the patch of every outgoing commit (git log -p over the pushed range, or the branch's full history on a first push) and pipes it to your command on stdin. Scanning per-commit — not just the endpoint tree diff — catches a secret that was added in one commit and removed in a later one but still ships in the pushed history.
  4. Allow only if the command exits 0 and its output is the all-clear (empty, or your clear_pattern). Otherwise the push is blocked and the command's output is shown.

This is fail-closed: a missing command, a non-zero exit, or a timeout blocks the push.

When it inspects, git-customs prints its progress to stderr so you can see it working (and know which check a slow command is stuck on):

git-customs: inspecting push to github.com/acme/oss (2 checks)
git-customs: → gitleaks
git-customs: → claude
git-customs: ✓ clean — 2 checks passed

A skipped push (private repo, branch deletion) stays silent; CUSTOMS_BYPASS=1 prints a one-line bypassed notice so a skipped gate is never a surprise.

Configuration — ~/.config/git-customs/config.toml

Each command receives the diff on stdin. ANY command works — it is not tied to AI. Commands are parsed with shell-style quoting, so a quoted argument may contain spaces. The push is allowed only if every command exits 0 AND its output is "clear" (empty, or exactly clear_pattern); the first that fails blocks the push.

# Combine engines with `commands` — they run in order, all must pass:
commands = [
  "gitleaks stdin --no-banner",                          # deterministic secrets
  'claude -p "List PII (emails, phone numbers, IDs, addresses, names) in added lines of the diff on stdin. One per line. If none, output exactly: OK"',
]
timeout       = "120s"          # per command; exceeding it blocks (fail-closed)
clear_pattern = "OK"            # empty output OR this exact string = clean

[repos]
public  = true                  # run on public repos
private = false                 # skip private repos

# explicit per-repo overrides always win over the public/private alias:
"github.com/jackchuka/blog"  = false   # public, but never inspect
"github.com/acme/secret-oss" = true    # private, but always inspect

[visibility]
skip_hosts  = ["github.example-corp.com"]  # always treated as private
skip_owners = ["my-private-org"]

The single-command shorthand still works when you only need one engine:

command = 'gitleaks stdin --no-banner'   # used only when `commands` is empty
Choosing an engine — reliability matters

git-customs only routes the diff; the engine decides what's a problem. Use a deterministic scanner (gitleaks) for the secret gate. An LLM is not a reliable hard gate: it is nondeterministic (the same diff can pass on one run and flag on the next) and prompt-injectable (text in the diff can steer its verdict). Reserve any AI command for PII, where no good deterministic tool exists, and treat it as best-effort rather than a guarantee.

How visibility is decided

Only when no explicit repo entry matches: git-customs asks gh for the true visibility of GitHub remotes, and falls back to a host heuristic (github.com / gitlab.com / bitbucket.org / codeberg.org are public) when gh isn't available. Anything it can't confidently classify is treated as private and skipped.

Overriding a block

CUSTOMS_BYPASS=1 git push     # skip inspection for this push
git push --no-verify          # skip all hooks

Coexisting with other hooks

A global core.hooksPath makes git read every hook type from one directory and stop looking in .git/hooks — which would silently disable a repo's other hooks. git-customs avoids that: it installs a shim for every standard hook.

  • pre-push → runs the customs gate, then chains to the hook git-customs shadowed (so an existing pre-push still runs).
  • Every other hook (pre-commit, commit-msg, post-checkout, …) → transparently delegates to the shadowed hook in shell, without even invoking git-customs.

The shadowed hook is the one git would otherwise have run: the repo's own core.hooksPath if it had set one (recorded at install time), else .git/hooks/<name>. So lefthook and husky keep working — lefthook writes into .git/hooks/, and a --here install over husky's .husky/ chains through to it. Your pre-commit lint/format hooks fire exactly as before; only pre-push gains the public-gate.

Caveat: a tool that sets its own repo-local core.hooksPath (husky does) overrides the global one, so git-customs won't run in that repo. Run git customs doctor to detect it, and git customs install --here to install git-customs for that single repo as well — it records the prior .husky/ path and chains every hook through to it.

Commands

Command Purpose
git customs install [--here] Install the hook globally (or for the current repo with --here)
git customs uninstall Remove the hook and restore prior core.hooksPath
git customs doctor Check the command is on PATH, the hook is active, and warn about repo-local bypass

License

MIT

Documentation

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
Package cmd implements the git-customs CLI.
Package cmd implements the git-customs CLI.
internal
chain
Package chain delegates to a repo's pre-existing hook shadowed by the global hooksPath.
Package chain delegates to a repo's pre-existing hook shadowed by the global hooksPath.
config
Package config loads git-customs TOML configuration.
Package config loads git-customs TOML configuration.
doctor
Package doctor reports git-customs configuration and coverage health.
Package doctor reports git-customs configuration and coverage health.
gitexec
Package gitexec wraps the real git and gh binaries.
Package gitexec wraps the real git and gh binaries.
gitrange
Package gitrange parses git pre-push input and builds the diff to inspect.
Package gitrange parses git pre-push input and builds the diff to inspect.
hook
Package hook orchestrates the git-customs pre-push inspection.
Package hook orchestrates the git-customs pre-push inspection.
install
Package install manages the global git-customs pre-push hook.
Package install manages the global git-customs pre-push hook.
mapping
Package mapping decides whether git-customs runs for a given repo.
Package mapping decides whether git-customs runs for a given repo.
runner
Package runner executes the configured inspection command and decides allow/block.
Package runner executes the configured inspection command and decides allow/block.
version
Package version holds build-time version information, stamped by GoReleaser.
Package version holds build-time version information, stamped by GoReleaser.
visibility
Package visibility resolves whether a git remote is public or private.
Package visibility resolves whether a git remote is public or private.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL