How the no-mistakes Post-Receive Hook Resolves Absolute Paths for the Gate Directory

The no-mistakes post-receive hook resolves absolute paths for the gate directory by first querying git rev-parse --show-toplevel, falling back to the hook's own location via dirname "$0", canonicalizing with cd … && pwd, and having the daemon re-verify the path through normalizeNotifyGatePath in internal/cli/daemon_cmd.go to prevent failures from relative path invocations.

When the kunchenguid/no-mistakes daemon receives a push notification, it must know the exact absolute filesystem location of the gate (the bare repository it monitors). Because Git can invoke the post-receive hook from a working directory that collapses to . (see issue #269), the hook cannot rely on relative paths. Instead, it implements a two-stage resolution strategy that guarantees a canonical absolute path reaches the daemon.

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

The post-receive hook script is generated programmatically by the PostReceiveHookScript function in internal/git/hook.go. This shell logic performs the first layer of path resolution before invoking the daemon.

Git-First Resolution Strategy

The hook prioritizes Git’s own understanding of the repository root. It executes git rev-parse --show-toplevel to obtain the absolute path to the top-level working directory. This works correctly even when the hook runs from a subdirectory within the repository.

Fallback to Hook Location

If Git cannot provide a path—such as when running in a bare repository without a working tree—the script falls back to the directory containing the hook file itself using $(dirname "$0"). This ensures a valid starting point regardless of the execution context.

Shell Canonicalization

Once a candidate directory is identified, the script canonicalizes it to an absolute path using standard shell utilities:

gate_dir=$(git rev-parse --show-toplevel 2>/dev/null || dirname "$0")
gate_dir=$(cd "$gate_dir" && pwd)
exec "$NM_BIN" daemon notify-push --gate "$gate_dir" ...

The cd "$gate_dir" && pwd sequence resolves all symbolic links and relative components (like ./ or ../), ensuring the --gate flag receives a clean, absolute path.

Stage 2: Daemon-Side Validation in internal/cli/daemon_cmd.go

The daemon does not blindly trust the --gate argument provided by the hook. As a safeguard against legacy hooks or unexpected environments, it re-processes the incoming path through normalizeNotifyGatePath.

The normalizeNotifyGatePath Safeguard

Located in internal/cli/daemon_cmd.go, this function validates and re-absolutizes the gate path:

func normalizeNotifyGatePath(gate string) (string, error) {
    if gate == "" {
        return "", errors.New("gate path missing")
    }
    // Resolve to an absolute, clean path
    abs, err := filepath.Abs(gate)
    if err != nil {
        return "", err
    }
    return abs, nil
}

The filepath.Abs call strips any remaining ./ prefixes and resolves the path against the current working directory if necessary. This defensive layer guarantees that the daemon stores a consistent, absolute reference to the gate directory, preventing the silent failures documented in AGENTS.md regarding issue #269.

Why Two Layers of Resolution Matter

The dual-resolution strategy provides defense in depth:

  • Hook-side logic handles the common case where Git can locate the repository, while the shell fallback ensures portability across bare and non-bare repositories.
  • Daemon-side normalization protects against edge cases where an older hook version or an unusual Git environment might pass a relative path.

Together, these mechanisms ensure that no-mistakes reliably resolves absolute paths for the gate directory regardless of how the post-receive hook was invoked.

Summary

  • The hook in internal/git/hook.go prefers git rev-parse --show-toplevel to find the repository root, falling back to dirname "$0" for bare repositories.
  • Shell canonicalization via cd … && pwd converts the candidate path to an absolute format before passing it via --gate.
  • The daemon’s normalizeNotifyGatePath function in internal/cli/daemon_cmd.go re-verifies the path using filepath.Abs to protect against relative path regressions.
  • This two-stage approach prevents the working-directory collapse issue described in issue #269, ensuring the pipeline trigger always targets the correct repository.

Frequently Asked Questions

What happens if Git cannot resolve the repository root in a post-receive hook?

If git rev-parse --show-toplevel fails (common in bare repositories without a working tree), the hook script falls back to $(dirname "$0"), which extracts the directory containing the hook file itself. This fallback ensures the script can still determine a valid starting directory for path canonicalization.

Why does the daemon re-validate the gate path with normalizeNotifyGatePath?

The daemon re-validates the path to defend against legacy hook versions or unusual Git environments that might send a relative path. By calling filepath.Abs in internal/cli/daemon_cmd.go, the daemon guarantees it stores a canonical absolute path, preventing silent failures when the daemon’s working directory differs from the hook’s invocation context.

Where is the post-receive hook script defined in the no-mistakes source code?

The hook script is generated by the PostReceiveHookScript function in internal/git/hook.go. This Go function returns a shell script template that embeds the resolution logic for the gate_dir variable before executing the daemon’s notify-push command.

How does absolute path resolution prevent issue #269?

Issue #269 describes a failure mode where Git invokes the post-receive hook with a working directory of ., causing relative paths to resolve incorrectly against the daemon’s working directory rather than the repository. By converting the gate directory to an absolute path in the hook (via cd … && pwd) and re-verifying it in the daemon, no-mistakes ensures the filesystem reference remains valid regardless of the invocation context.

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 →