How the post-receive Hook Resolves Absolute Gate Paths in no-mistakes

The post-receive hook derives an absolute gate path by querying Git's core.hooksPath configuration—or falling back to the script's own directory—and then relies on the daemon's normalizeNotifyGatePath function to canonicalize the path and eliminate relative references like . that would otherwise cause pipelines to silently fail.

The no-mistakes CI/CD system triggers pipelines when developers push to gate repositories (bare repositories that drive the pipeline). When Git invokes the post-receive hook, the current working directory is often ambiguous; in bare repositories, $(pwd) resolves to . (see issue #269), making relative paths unreliable. To prevent the daemon from receiving an unusable path, the hook implements a two-step resolution strategy that guarantees an absolute, canonical gate directory.

Why Relative Paths Fail in Bare Repositories

Git hooks execute in the context of the repository, but the working directory is not guaranteed to be the repository root. In bare repositories, the working directory is typically the repository directory itself, yet shell commands may resolve . differently depending on the Git version or invocation method. If the hook passed a relative path like . to the daemon, the daemon would attempt to locate the gate relative to its own process directory, causing the pipeline to never start. This vulnerability requires the hook to resolve an absolute path before notifying the daemon.

Step 1: Hook-Side Resolution in internal/git/hook.go

The PostReceiveHookScript function (called by InstallPostReceiveHook) generates a shell script that performs the first line of defense. The script uses a priority-based approach to locate the gate directory, then converts it to an absolute path before invoking the daemon.

Checking core.hooksPath First

The script attempts to read Git's core.hooksPath configuration, interpreting it relative to the repository's git directory. This supports custom hook installation paths.

Fallback to the Script Directory

If core.hooksPath is unset or empty, the script falls back to $(dirname "$0")—the directory containing the hook file itself. It then uses cd "$gate_dir" && pwd to resolve any symbolic links and return a clean absolute path.

#!/bin/sh

# no-mistakes post-receive hook

# Resolve gate directory:

#   1. Prefer git's core.hooksPath if configured

#   2. Fallback to the directory of this script

#   3. Convert to an absolute path

gate_dir="$(git -C "$(dirname "$0")/.." config --get core.hooksPath 2>/dev/null)"
if [ -z "$gate_dir" ]; then
    gate_dir="$(dirname "$0")"
fi
gate_dir="$(cd "$gate_dir" && pwd)"

# Notify the daemon (non‑blocking)

"$NM_BIN" daemon notify-push --gate "$gate_dir" &

Step 2: Daemon-Side Normalization in internal/cli/daemon_cmd.go

Even with the hook's precautions, the daemon must sanitize the incoming --gate argument. The normalizeNotifyGatePath function in internal/cli/daemon_cmd.go serves as a final safeguard, handling legacy hooks that may still pass relative paths and cleaning up any filesystem irregularities.

The function checks filepath.IsAbs(gate). If the path is relative, it resolves it against the daemon's current working directory using filepath.Abs. Finally, it calls filepath.EvalSymlinks to collapse any symbolic links into their canonical targets.

func normalizeNotifyGatePath(gate string) (string, error) {
    // If the user supplied a relative path (e.g. "."), resolve it.
    if !filepath.IsAbs(gate) {
        abs, err := filepath.Abs(gate)
        if err != nil {
            return "", err
        }
        gate = abs
    }
    // Clean up any symlinks / redundant elements.
    return filepath.EvalSymlinks(gate)
}

Legacy Hook Compatibility

Older versions of no-mistakes allowed hooks to pass relative paths such as . directly to the daemon. The normalization logic in internal/cli/daemon_cmd_test.go verifies that these legacy values are correctly converted to absolute paths, preventing the "pipeline silently never starts" regression described in issue #269.

Summary

  • The hook script (internal/git/hook.go) prefers core.hooksPath but falls back to its own directory, using cd && pwd to guarantee an absolute path before calling the daemon.
  • The daemon (internal/cli/daemon_cmd.go) runs normalizeNotifyGatePath to convert any remaining relative paths (including legacy . arguments) to absolute paths and resolves symlinks via filepath.EvalSymlinks.
  • Together, these mechanisms ensure the daemon receives a canonical absolute path to the gate repository, eliminating ambiguity from bare repository working directories and preventing silent pipeline failures.

Frequently Asked Questions

What is a gate in no-mistakes?

A gate is a bare Git repository configured with a post-receive hook that triggers the no-mistakes pipeline daemon. When you push to the gate, the hook notifies the daemon to start processing the new commit.

Why can't the hook simply use $(pwd) to find the gate?

In bare repositories, the shell's current working directory may resolve to . rather than the actual filesystem path, particularly when Git invokes the hook from an unexpected directory (issue #269). Relying on $(pwd) risks passing a relative reference that the daemon cannot resolve correctly.

The normalizeNotifyGatePath function calls filepath.EvalSymlinks after converting the path to absolute form. This collapses any symbolic links into their canonical targets, ensuring the daemon compares the actual inode paths when matching the gate to registered repositories.

What happens if core.hooksPath is set to a relative value?

The hook script resolves core.hooksPath relative to the repository's git directory before converting it to an absolute path with cd && pwd. If the value is invalid or empty, the script falls back to the directory containing the hook file itself.

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 →