Skip to content

jbrahy/git-wrapper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

git-wrapper

A transparent git security shim. It installs ahead of the real git on your PATH, inspects every git command for git-native code-execution traps, hard-blocks the dangerous ones before git runs, warns on the suspicious ones, and is otherwise completely invisible — same output, same exit codes, negligible overhead.

$ git clone https://github.com/someone/cool-tool
Cloning into 'cool-tool'...
[BLOCK] active-hook
    cool-tool/.git/hooks/post-checkout
    value: post-checkout
    why:   an installed git hook runs automatically on git operations
git-wrapper: cloned repo "cool-tool" contains BLOCK-level issues. Do not run commands inside it.

Why this exists

There is a steady stream of malware distributed through GitHub: fake "cracked" software, typosquatted packages, trojanized proof-of-concept exploits, and ordinary looking repositories that attack you the moment you interact with them. A common and under-appreciated class of these attacks doesn't need you to run the project at all — it abuses git itself.

The important and counter-intuitive fact: git clone does not execute a remote repo's hooks. Hooks live in .git/hooks/ and are never transmitted on clone. So the danger isn't usually the clone — it's the moments right after:

  • Poisoned .git/config. A repo delivered as an archive (zip/tarball) or a synced folder can carry its own .git/config. Keys like core.fsmonitor, core.hooksPath, and core.sshCommand name a command that git runs on your very next git operation — even git status, even your editor silently running git in the background. This is remote code execution triggered by looking at the repo.
  • Active hooks that arrive via an archive, or get wired up by a hook-manager package (husky, pre-commit) during npm install / pip install.
  • Install-time scriptspostinstall in package.json, custom cmdclass in setup.py — that run automatically during dependency installation.
  • Committed binaries and obfuscated payloads smuggled into a "source" repo.

git-wrapper is positioned exactly where these fire. Because it sits in front of every git invocation, it can read .git/config before handing control to git — closing the core.fsmonitor "next command" window that a one-time scanner would miss.

Why you should use it

  • It catches the attacks you can't see coming. The config-execution vector runs before you've typed anything dangerous. A shim is the only place you can intercept it.
  • Zero workflow change. It is git. Commands you already run behave identically; there's no new tool to remember and no flags to learn for everyday use.
  • Fails open, never locks you out. If the wrapper hits any internal error — or even panics — it logs and falls through to real git. A bug in the shim can never break your git. (This is an enforced, tested invariant.)
  • Strict but escapable. Dangerous findings block by default, but a single --force overrides and remembers your decision so you're not nagged again.
  • Tiny and dependency-free. One ~3–4 MB static Go binary, standard library only, ~2 ms overhead per call. Nothing to trust but the source in this repo.

Who benefits most: anyone who clones, reviews, or tries out code they didn't write — security researchers, open-source maintainers triaging PRs, developers grabbing tools and dependencies, students, CTF players. It is defense in depth, not a silver bullet: the strongest habit is still to run first-time/untrusted installs in a disposable VM. git-wrapper backs that up by catching the git-layer traps automatically, every time.


How it works

git-wrapper is a single binary named git, placed in a directory earlier on your PATH than the system git. On each call it:

  1. Resolves the real git once (the next git on PATH that isn't itself).
  2. Cheap config gate — every command. If you're inside a repo, it reads only .git/config (sub-millisecond) and blocks if it finds an execution vector. This is the primary core.fsmonitor defense, and it correctly follows the gitdir: file used by submodules and linked worktrees so those can't slip past.
  3. Full scan — content-acquiring commands only. For commands that pull new tree content (see the list under Configuration), it runs the hooks / install-script / binary / obfuscation checks. clone is scanned after it runs (the repo doesn't exist until then); everything else is scanned before. Results are fingerprint-cached so an unchanged repo isn't rescanned.
  4. Hands off to real git, byte-for-byte: your args, stdin/stdout/stderr, and exit code are passed through unchanged.

Two invariants are enforced and tested: fail-open (any wrapper error → real git still runs) and transparent passthrough (non-scanned commands are indistinguishable from bare git, and --force is never stripped from the args git receives).

What it catches

Check What it looks for Severity
Dangerous git config core.fsmonitor, core.hooksPath, core.sshCommand, !-prefixed core.pager/core.editor, !-prefixed aliases, url.*.insteadOf redirects BLOCK
Active hooks non-.sample files in .git/hooks/ BLOCK
Checked-in hooks .githooks/, .husky/, .pre-commit-config.yaml, READMEs instructing core.hooksPath WARN
Install scripts package.json preinstall/install/postinstall/prepare, setup.py cmdclass, `curl … sh` patterns
Binaries & obfuscation committed .exe/.dll/.so/.dylib/.bin/.msi, Discord/Telegram webhook URLs, eval(atob()), long base64 blobs WARN

Every finding prints its severity, the file:line, the offending value, and a one-line explanation of why it's dangerous.

  • BLOCK stops before real git runs. You must neutralize the issue or --force.
  • WARN prints but does not stop — it's there to inform, e.g. "remember to run npm install --ignore-scripts here."

Install

Requires Go 1.26+ and macOS or Linux.

git clone git@github.com:jbrahy/git-wrapper.git
cd git-wrapper
sh install.sh

install.sh builds the binary into ~/.local/bin/git-wrapper/git and prints the line to add to your shell profile. Put that directory at the front of your PATH:

# ~/.zshrc or ~/.bashrc
export PATH="$HOME/.local/bin/git-wrapper:$PATH"

Verify the shim is active:

command -v git          # should print …/.local/bin/git-wrapper/git
git --version           # should print the normal "git version …"

To install elsewhere, set PREFIX (see Configuration).


Configuration

git-wrapper is intentionally low-config: it works out of the box and is tuned through a small set of environment variables, one flag, and one state file. There is no config file to write in v1 (per-rule severity tuning is planned for v2 — see Limitations).

Environment variables

Variable Used by Default Effect
GIT_WRAPPER_DISABLE runtime unset Set to 1 to fully bypass scanning for that invocation — pure passthrough to real git. The global escape hatch for scripts, CI, or a command the wrapper gets in the way of: GIT_WRAPPER_DISABLE=1 git status.
XDG_CONFIG_HOME runtime $HOME/.config Base directory for git-wrapper's state. The allowlist lives at $XDG_CONFIG_HOME/git-wrapper/allowlist.json.
HOME runtime Used to locate the config dir when XDG_CONFIG_HOME is unset.
PATH runtime Must list the wrapper's directory before the real git's. This is what makes the shim active; see Install.
PREFIX install.sh $HOME/.local/bin/git-wrapper Where install.sh builds and places the git binary, e.g. PREFIX=/usr/local/libexec/git-wrapper sh install.sh.

The --force flag (override)

When a command is BLOCKed, re-run it with --force (or -f) to override:

git fetch --force          # runs despite a BLOCK, and remembers this repo

--force is detected by the wrapper but not stripped — it is still passed to real git. This means it's intended for commands that already accept or ignore a trailing --force (clone, fetch, pull, checkout, push). For a command where --force isn't valid (e.g. git status), use GIT_WRAPPER_DISABLE=1 instead.

The allowlist (remembering your decisions)

The first --force on a repo records it so you aren't re-prompted on every subsequent command. Approved repos run completely silently (no findings printed) until their content changes.

  • Location: $XDG_CONFIG_HOME/git-wrapper/allowlist.json (default ~/.config/git-wrapper/allowlist.json).

  • Format: a JSON object mapping each repo's absolute path to a content fingerprint:

    {
      "/Users/you/src/some-repo": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
    }
  • The fingerprint is content-based — a SHA-256 over the repo path plus the bytes of .git/HEAD, .git/config, and the sorted hook filenames. If any of those change (i.e., the repo is re-poisoned), the fingerprint no longer matches and the warnings return automatically. It deliberately does not use file mtimes, which an attacker could forge.

  • To revoke an approval: delete that repo's line (or the whole file). The next command re-scans from scratch.

  • To pre-approve or audit in bulk: the file is plain JSON — edit it directly.

Which commands trigger a full scan

The cheap .git/config gate runs on every command. The heavier hooks/install/binary scan runs only on commands that can bring new tree content into your working copy:

clone   fetch   pull   checkout   merge   switch   submodule
restore reset   stash  worktree   am      apply

Everything else (status, log, commit, diff, …) skips the full scan entirely.

Tuning the noise (today vs. v2)

v1 keeps the rule set and severities fixed in the binary. The levers you have today:

  • Approve a repo you trust with one --force → it goes silent.
  • Bypass entirely for a command or a whole script with GIT_WRAPPER_DISABLE=1.

Per-rule enable/disable and severity overrides via a config file are the first item on the v2 roadmap.


Security model & limitations

Threat model. git-wrapper defends against repository-supplied git-native execution (malicious config, hooks, install scripts) and flags suspicious committed content. It assumes you control your own PATH and binaries. If an attacker can already place a fake git ahead of the wrapper on your PATH, that's outside the model — they already have code execution.

It is a layer, not a sandbox. It detects and blocks known git-layer traps; it does not contain code you choose to run. For genuinely untrusted projects, still do first-time installs in a disposable VM with no real SSH keys, .env, browser profiles, or wallets present.

Limitations / roadmap

  • macOS and Linux only. Windows PATH-shim mechanics differ and are deferred.
  • No repo-reputation checks yet (stars/age/typosquatting). Deferred to v2 — needs network access and a GitHub token.
  • WARN checks are heuristic and can have false positives (e.g. a legitimate minified asset that looks like a base64 blob). They never block, only inform.
  • No per-rule config file yet (v2).
  • Submodule tree recursion depth for the full scan is limited in v1 (the config gate already follows submodule/worktree gitdir: files).

How it's built

Small, focused Go packages, standard library only:

cmd/git/          # entrypoint: resolve real git, gate, scan, exec — fails open
internal/resolve/ # find the real git; resolve repo root + (possibly file-based) gitdir
internal/scan/    # one file per check (config, hooks, install, binary) + a registry
internal/report/  # Finding + Severity (BLOCK/WARN) formatting
internal/state/   # --force allowlist + content fingerprint
install.sh        # build onto PATH and print setup instructions

Run the test suite:

go test ./...
go vet ./...

The design rationale and the task-by-task implementation plan are committed under docs/superpowers/ if you want to see how and why it was built.

Contributing

Issues and PRs welcome. Please keep the two invariants intact (fail-open and transparent passthrough), add tests for any new check, and run go test ./... && go vet ./... before opening a PR.

License

MIT — a permissive license. You may use, copy, modify, merge, publish, distribute, sublicense, and sell copies, including in closed-source and commercial products, with no obligation beyond preserving the copyright notice. In short: everyone is free to do what they want with it.

About

Transparent git security shim that blocks malicious repos before they execute code on your machine — catches poisoned .git/config (core.fsmonitor), hidden hooks, and install-script traps. Zero workflow change, fails open. Stop being a victim of GitHub malware. Go · macOS/Linux.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors