snag

command module
v0.16.2 Latest Latest
Warning

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

Go to latest
Published: Apr 7, 2026 License: MIT Imports: 14 Imported by: 0

README

snag

A composable git hook policy kit.

CI

Every repo needs git hooks — formatting checks, secret scanning, commit message standards. Most teams solve this one repo at a time: a shell script here, a husky config there, maybe a pre-commit framework if someone sets it up.

You could write a shell one-liner for each check. Then you need it in three hook phases, each with slightly different behavior, across 40 repos. The one-liner is now a project.

snag is that project, kept small on purpose.

What you get

  • A curated set of lefthook recipe files covering common checks (content policy, secret scanning, formatting, linting). Each recipe is a standalone fragment any repo can pull in via lefthook remotes.

  • A small Go CLI (snag) for per-repo content policy enforcement via snag.toml. For checks where no good off-the-shelf tool exists yet.

Install

go install
go install github.com/dpritchett/snag@latest
mise
mise use -g go:github.com/dpritchett/snag@latest
Binary releases

Pre-built binaries are available on the Releases page (via GoReleaser).

Recipe-only usage

If you only want the lefthook recipes (gitleaks, shellcheck, Go checks) and don't need the snag CLI, you don't need to install the binary at all — just point your lefthook remotes at this repo.

Quick start

If you already have a lefthook config, the fastest path is:

snag install         # adds the snag remote to your lefthook config
lefthook install     # activates the hooks

Or add the remote manually:

# lefthook.yml
remotes:
  - git_url: https://github.com/dpritchett/snag.git
    ref: v0.4.3
    configs:
      - recipes/lefthook-snag-filter.yml
      - recipes/lefthook-gitleaks.yml

Add a snag.toml to your repo root:

# snag.toml — committed, version-controlled team policy
min_version = "0.10.0"

[block]
diff = ["TODO", "HACK", "DO NOT MERGE"]
msg  = ["WIP", "fixup!", "squash!"]
branch = ["main", "master"]

Run lefthook install and you're set.

Recipes

Recipe Hook phase(s) What it does Requires
lefthook-snag-filter.yml pre-commit, commit-msg, pre-push Content policy via snag.toml snag CLI
lefthook-gitleaks.yml pre-commit Secret scanning gitleaks
lefthook-go.yml pre-commit go fmt, go vet, go test Go toolchain
lefthook-shellcheck.yml pre-commit Lint staged shell scripts shellcheck

Each recipe is independent. A repo can pull one or all of them. Pin ref to a tag (e.g. ref: v1.0.0) for stability, or use main to track latest.

lefthook-go.yml intentionally does not use glob: "*.go". Its commands run repo-wide already, and a root-only glob would skip staged changes in nested Go packages like cmd/ and internal/.

Local overrides. Any repo can add a lefthook-local.yml to extend or override what the remote recipes provide. lefthook merges local config on top of remote config automatically.

More lefthook examples
# JS project — snag-filter + gitleaks only
remotes:
  - git_url: https://github.com/dpritchett/snag.git
    ref: main
    configs:
      - recipes/lefthook-snag-filter.yml
      - recipes/lefthook-gitleaks.yml
# Go project — full stack
remotes:
  - git_url: https://github.com/dpritchett/snag.git
    ref: main
    configs:
      - recipes/lefthook-snag-filter.yml
      - recipes/lefthook-gitleaks.yml
      - recipes/lefthook-go.yml

CLI usage

snag check diff        # pre-commit: scan staged changes
snag check msg FILE    # commit-msg: clean trailers, reject body matches
snag check push        # pre-push: scan all unpushed commits
snag audit             # scan git history for policy violations
snag install           # add/update snag remote in lefthook config
snag version           # print version and exit

All three perform case-insensitive substring matching and exit 0 (clean) or 1 (match found, with a human-readable error).

By default, snag walks up from the current directory to the filesystem root, loading every snag.toml it finds and merging all patterns.

snag install

Adds or updates the snag remote in your lefthook config. Finds lefthook.yml, lefthook.yaml, .lefthook.yml, or .lefthook.yaml. Pins the ref to the running binary's version.

$ snag install
Added snag v0.4.3 remote to lefthook.yaml
Run `lefthook install` to activate.

If a snag remote already exists at an older version, it updates the ref in place without touching the rest of the file. If it's already current, it does nothing.

After installing, snag install runs an informational snag audit to flag any existing violations in recent history. These are printed as warnings and don't block the install.

snag check diff
$ snag check diff
snag: match "do not merge" in staged diff
snag check msg

Two-pass approach: first strips git trailer lines (Key: Value) matching the pattern list, rewriting the file in place. Then checks the remaining message body.

$ snag check msg .git/COMMIT_EDITMSG
snag: removed 1 trailer line(s)
$ snag check msg .git/COMMIT_EDITMSG
snag: match "fixme" in commit message
  to recover: git commit -eF .git/COMMIT_EDITMSG
snag check push

Scans all unpushed commits — both messages and diffs. The safety net for anything that slipped past per-commit hooks.

$ snag check push
snag: 4 patterns checked against 3 commits
snag audit

Scans git history for policy violations — the retroactive check for repos with pre-snag history. Checks commit messages against msg patterns and diffs against diff patterns, then reports every match.

$ snag audit
snag: scanning 10 commits...

  abc1234 — "Add integration config"
    diff: match "HACK" in commit diff

  def5678 — "Update deploy script"
    msg:  match "fixup!" in commit msg
    diff: match "HACK" in commit diff

snag: 3 violations found in 2 of 10 commits

Exits 1 when violations are found, 0 when clean — CI-friendly.

snag audit                    # config value or last 10 commits
snag audit --limit 10         # last 10 commits
snag audit --limit 0          # full history
snag audit main..HEAD         # explicit range
snag audit -q                 # summary line + exit code only

Set the default audit window in snag.toml or snag-local.toml:

[audit]
limit = 25

audit.limit = 0 scans full history by default. CLI --limit still wins when you need a one-off override.

Flags
--quiet             # suppress informational output
--version           # print version and exit
Color output

snag uses color when connected to a terminal and suppresses it in pipes and CI environments. This follows the NO_COLOR standard:

NO_COLOR=1 snag audit          # force colors off (any value works)
CLICOLOR_FORCE=1 snag audit    # force colors on, even in pipes/CI

No snag-specific color flag needed — if your terminal supports color, you get color. If you're piping to a file or running in CI, colors are stripped automatically.

Shell completions

snag ships tab completion for fish, bash, and zsh:

# fish
snag completion fish > ~/.config/fish/completions/snag.fish

# bash (macOS)
snag completion bash > $(brew --prefix)/etc/bash_completion.d/snag

# bash (Linux)
snag completion bash > /etc/bash_completion.d/snag

# zsh
snag completion zsh > "${fpath[1]}/_snag"

Restart your shell (or source the file) to activate.

Configuration

snag.toml — team policy

The primary config format. Check it into your repo alongside lefthook.yml. Each hook phase gets its own pattern list:

# snag.toml — committed, version-controlled team policy
min_version = "0.10.0"

[block]
diff = ["TODO", "NOCOMMIT"]
msg  = ["WIP", "fixup!", "squash!"]
# push: omit to inherit the union of diff + msg as a safety net
branch = ["main", "master"]

Generate a starter config with snag init:

snag init              # creates snag.toml with common patterns
snag init --local      # creates snag-local.toml for personal patterns
snag-local.toml — personal/sensitive patterns

A gitignored overlay for patterns you don't want committed. Same format as snag.toml, merged additively — it only adds patterns, never overrides.

# snag-local.toml — gitignored, personal/sensitive patterns
[block]
diff = ["clientname", "internal-codename"]
msg  = ["clientname"]

Both files are checked at each directory level during the config walk. Patterns accumulate as snag walks up to the filesystem root:

~/projects/acme/snag.toml            ← team policy: "TODO", "WIP"
~/projects/acme/snag-local.toml      ← personal: "clientname"
~/projects/acme/api/snag.toml        ← additional repo-specific patterns
~/projects/acme/api/snag-local.toml  ← additional personal patterns
~/projects/acme/api/service/         ← no config here, protected by all above

snag ships no default patterns — that's a policy decision, not a tool decision.

Hook runner examples

The snag CLI is hook-runner-agnostic. The recipes target lefthook, but the CLI works anywhere:

lefthook
pre-commit:
  commands:
    snag-filter:
      run: snag check diff

commit-msg:
  commands:
    snag-filter:
      run: snag check msg {1}

pre-push:
  commands:
    snag-filter:
      run: snag check push
husky
{
  "husky": {
    "hooks": {
      "pre-commit": "snag check diff",
      "commit-msg": "snag check msg $HUSKY_GIT_PARAMS",
      "pre-push": "snag check push"
    }
  }
}
pre-commit (Python framework)
# .pre-commit-config.yaml
repos:
  - repo: local
    hooks:
      - id: snag-diff
        name: snag content filter
        entry: snag check diff
        language: system
        stages: [pre-commit]
      - id: snag-msg
        name: snag commit message
        entry: snag check msg
        language: system
        stages: [commit-msg]
      - id: snag-push
        name: snag push check
        entry: snag check push
        language: system
        stages: [pre-push]
Raw githooks
#!/bin/sh
# .git/hooks/pre-commit
snag check diff

direnv canary

If you use direnv, add a canary check to .envrc so you never forget to wire up hooks:

if [ -f snag.toml ] && ! [ -f lefthook.yml ]; then
  printf '\033[33m!! %s has a snag.toml but no lefthook.yml\033[0m\n' \
    "$(basename "$PWD")"
elif [ -f lefthook.yml ] && \
     ! grep -q lefthook .git/hooks/pre-commit 2>/dev/null; then
  printf '\033[33m!! lefthook hooks not installed — run: lefthook install\033[0m\n'
fi

Every time you cd into a repo, direnv tells you if hooks aren't wired up.

License

MIT — see LICENSE.

Documentation

The Go Gopher

There is no documentation for this package.

Jump to

Keyboard shortcuts

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