How the no-mistakes Git Hook Intercepts git push to Route Through the Gate Remote

The no-mistakes Git hook intercepts pushes by installing a post-receive script in a bare "gate" repository that resolves push metadata and invokes no-mistakes daemon notify-push to trigger the pipeline without blocking the push operation.

The kunchenguid/no-mistakes tool creates a dedicated gate — a bare Git repository that acts as the pipeline’s entry point. When you push to the remote named no-mistakes, Git fires a custom post-receive hook that captures the push details and forwards them to the daemon, ensuring every commit runs through your review and CI pipeline before reaching the main repository.

The Gate Remote Architecture

no-mistakes establishes a gate remote that points your working copy to a local bare repository. This remote, always named no-mistakes, serves as the interception point.

The setup is orchestrated by functions in internal/gate/gate.go:

  • ensureWorkingRemote (lines 19–21) adds the no-mistakes remote to your working repository if it does not exist, or repairs it during refresh operations.
  • Init and InitWithFork (lines 28–38) create the bare gate repository under $NM_HOME/repos/<repo-id>.git, install the hook, and wire up the remote.

This architecture guarantees idempotency — running init multiple times only adds the remote when missing, never overwriting user-managed remotes.

The Post-Receive Hook Mechanism

When you execute git push no-mistakes <branch>, Git updates the bare gate repository and triggers the post-receive hook installed at <gate>/.git/hooks/post-receive. The hook is generated by PostReceiveHookScript in internal/git/hook.go (lines 15–89).

The hook operates non-blocking; Git ignores its exit status, meaning your push succeeds even if the pipeline daemon fails to start.

Resolving the Gate Directory

The hook must reliably locate the gate directory even when the current working directory collapses to . (issue #269). It uses:

GATE_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || pwd)

This guarantees an absolute path to the bare repository, ensuring the daemon always receives a valid --gate argument regardless of how Git invokes the hook.

Collecting Push Metadata

The hook parses the push data from Git’s standard input and environment variables. It collects:

  • Old SHA ($oldrev) and new SHA ($newrev)
  • Reference name ($refname)
  • Push options from GIT_PUSH_OPTION_COUNT and GIT_PUSH_OPTION_n variables

The script builds the argument list incrementally:

set -- --gate "$GATE_DIR" \
       --ref "$refname" \
       --old "$oldrev" \
       --new "$newrev"

# Append any push-options

while [ "$i" -lt "${GIT_PUSH_OPTION_COUNT:-0}" ]; do
    opt=$(printenv "GIT_PUSH_OPTION_$i" 2>/dev/null || :)
    set -- "$@" --push-option "$opt"
    i=$((i + 1))
done

Invoking the Daemon

With arguments assembled, the hook invokes the daemon:

out=$(NM_HOOK_HELPER=1 "$NM_BIN" daemon notify-push "$@" 2>&1)
status=$?

The NM_HOOK_HELPER=1 environment variable signals to the binary that it is running from a hook context, disabling interactive UI elements. The daemon notify-push command (registered in internal/daemon/daemon.go) receives the gate path, ref names, and options to create a new pipeline run.

If the daemon exits non-zero, the hook logs the failure to $GATE_DIR/notify-push.log (lines 64–66 in hook.go) and echoes the error to stderr so the pushing client sees it. On success, it prints a concise ASCII banner indicating the pipeline has started.

Step-by-Step Push Interception Flow

  1. Initialization: no-mistakes init creates the bare gate, installs the post-receive hook via git.InstallPostReceiveHook, and adds the no-mistakes remote via ensureWorkingRemote.

  2. Push Execution: You run git push no-mistakes HEAD:refs/heads/feature. Git contacts the bare gate and updates the ref.

  3. Hook Fires: The post-receive script resolves $GATE_DIR using git rev-parse --absolute-git-dir.

  4. Metadata Extraction: The hook reads the old SHA, new SHA, and ref name from Git’s hook input, then iterates through GIT_PUSH_OPTION_* variables to capture push options.

  5. Daemon Notification: The hook executes no-mistakes daemon notify-push with the --gate, --ref, --old, --new, and --push-option flags.

  6. Error Handling: Failures are written to notify-push.log inside the gate directory and printed to stderr. The hook always exits cleanly to avoid blocking the Git operation.

  7. Pipeline Start: The daemon creates a new run in the database, beginning the review → test → lint pipeline using the gate’s bare repository as the source of truth.

Why the Gate Remote Is Essential

The gate remote provides three critical guarantees:

  • Isolation: The gate is a bare repository; its history never contains unreviewed developer commits, ensuring the pipeline always validates clean states.
  • Deterministic Source: All pipeline steps read from the gate’s remote-tracking refs, guaranteeing they process the exact commit the daemon received.
  • Non-Destructive Setup: The ensureWorkingRemote logic only modifies the no-mistakes remote, leaving your origin and other remotes untouched.

Usage Example

Initialize the gate and push a feature branch:


# Initialize the gate (creates bare repo, installs hook, adds remote)

$ no-mistakes init

# Create and commit a feature

$ git checkout -b feat/optimize
$ echo "optimized code" > main.go
$ git add main.go && git commit -m "optimize performance"

# Push to the gate remote

$ git push no-mistakes HEAD:refs/heads/feat/optimize

The hook outputs a banner confirming the intercept:

_  _ ____    _  _ _ ____ ___ ____ _  _ ____ ____
...
* Pipeline started
Run no-mistakes to review.

If the daemon fails to start, check the persistent log:

$ cat ~/.no-mistakes/repos/<repo-id>.git/notify-push.log

Summary

  • The gate remote (no-mistakes) routes pushes through a bare repository that acts as the pipeline entry point.
  • The post-receive hook in internal/git/hook.go intercepts pushes, resolves the absolute gate path, and forwards metadata to the daemon.
  • Non-blocking execution ensures Git pushes succeed regardless of pipeline daemon status.
  • Push metadata including SHAs, ref names, and push options are passed via daemon notify-push arguments.
  • Error logs persist in notify-push.log inside the gate directory for debugging daemon connectivity issues.

Frequently Asked Questions

Does the hook block my push if the pipeline fails?

No. The hook operates non-blocking, meaning Git ignores the exit status of the post-receive script. Your push succeeds immediately, and any pipeline failures are logged to notify-push.log inside the gate directory while the error message is echoed to your terminal.

Where does the hook log errors when the daemon is unreachable?

The hook writes failures to $GATE_DIR/notify-push.log (as implemented in hook.go lines 64–66), where $GATE_DIR is the absolute path to the bare gate repository. You can view this log to diagnose why the no-mistakes daemon notify-push command failed.

What is the NM_HOOK_HELPER=1 environment variable for?

This variable tells the no-mistakes binary that it is being invoked from a Git hook rather than an interactive terminal. When set, the CLI disables interactive UI elements and streams output appropriately for the hook context, as seen in the hook script generated by PostReceiveHookScript.

How does the hook handle Git push options?

The hook iterates through the GIT_PUSH_OPTION_COUNT and GIT_PUSH_OPTION_n environment variables (where n is an index), appending each option to the daemon command via --push-option flags. This allows the pipeline to receive additional metadata, such as CI configuration flags, passed during git push --push-option.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →