How the Post-Receive Hook Gate Path Is Resolved and Normalized in no-mistakes

The no-mistakes daemon normalizes the post-receive hook's gate path through a five-step validation pipeline that converts relative paths to absolute, resolves symlinks, and verifies the gate directory structure, ensuring the daemon always receives a canonical filesystem location even when invoked from bare repositories.

When a push triggers the post-receive hook in the kunchenguid/no-mistakes repository, the daemon must reliably determine which gate repository to notify. Because Git may invoke hooks from bare repositories where $(pwd) resolves to a single dot or relative path, the system must validate and sanitize the incoming path before processing. The normalization logic resides in internal/cli/daemon_cmd.go and protects against ambiguous working directories, symlink attacks, and legacy hook configurations.

The Five-Step Normalization Pipeline

The normalizeNotifyGatePath function in internal/cli/daemon_cmd.go implements a defensive sequence to ensure the daemon receives a trustworthy path. Each step addresses a specific failure mode encountered when the post-receive hook executes in different Git environments.

Step 1: Absolute Path Detection

The function first checks if the incoming --gate value is already an absolute path using filepath.IsAbs. If the path is absolute, it bypasses relative resolution and proceeds directly to symlink evaluation. This optimization avoids unnecessary filesystem calls when the hook supplies a fully qualified path.

Step 2: Relative to Absolute Conversion

For relative paths (including the problematic "." from bare repositories), the code invokes filepath.Abs to resolve the path against the current working directory. This guarantees a concrete filesystem location regardless of where Git invoked the hook.

After obtaining an absolute path, the function passes it through filepath.EvalSymlinks to collapse any symbolic links. This prevents indirect paths that could potentially traverse outside the intended gate directory or create ambiguity about which physical directory represents the gate.

Step 4: Gate Directory Validation

The resolved path undergoes verification via isGateDir, which checks for the expected gate layout—specifically the presence of a hooks directory containing the post-receive script and the gate's internal database files. If the directory fails this validation, the function returns an error to prevent the daemon from operating on an invalid repository.

Step 5: Git Fallback for Legacy Compatibility

If the --gate flag is empty or the resolved path fails gate validation, the system executes git rev-parse --show-toplevel to discover the repository root. This fallback ensures backward compatibility with older installations that relied on core.hookspath configurations, addressing issue #269 where legacy hooks provided insufficient path information.

Implementation in normalizeNotifyGatePath

The following Go code from internal/cli/daemon_cmd.go illustrates the complete normalization logic:

func normalizeNotifyGatePath(gate string) (string, error) {
    // 1️⃣  Already absolute?
    if filepath.IsAbs(gate) {
        return gate, nil
    }

    // 2️⃣  Make absolute relative to cwd
    abs, err := filepath.Abs(gate)
    if err != nil {
        return "", err
    }

    // 3️⃣  Resolve any symlinks
    resolved, err := filepath.EvalSymlinks(abs)
    if err != nil {
        return "", err
    }

    // 4️⃣  Verify the directory looks like a gate
    if !isGateDir(resolved) {
        return "", fmt.Errorf("%s is not a valid no‑mistakes gate", resolved)
    }

    return resolved, nil
}

The function returns a canonical, absolute, symlink-free path that points to a valid gate repository, eliminating the "bare $(pwd)" pitfall where Git invokes the hook from a meaningless working directory.

How the Hook Passes the Gate Path

The post-receive hook script itself is generated by internal/git/hook.go during installation. The template embeds the gate path as an environment variable NM_GATE:

#!/bin/sh

# no-mistakes post‑receive hook

# Notifies the daemon of the push. Non‑blocking: post‑receive exit code is ignored.

exec "$NM_BIN" daemon notify-push --gate "$NM_GATE"

During installation, NM_GATE is set to the absolute gate directory using filepath.Join(gateRoot, "hooks", "post-receive"). When the daemon receives this value, it processes it through normalizeNotifyGatePath to handle edge cases where older hooks might supply relative paths or where the hook executes in unexpected working directories.

Summary

  • Absolute enforcement: The daemon converts all relative paths to absolute using filepath.Abs to eliminate ambiguity from bare repository invocations.
  • Symlink protection: filepath.EvalSymlinks collapses symbolic links to prevent directory traversal outside the intended gate.
  • Structure validation: The isGateDir check ensures the path contains the required hooks/post-receive script and internal database.
  • Legacy support: The git fallback mechanism uses git rev-parse --show-toplevel to support older installations referenced in issue #269.
  • Key files: The normalization logic lives in internal/cli/daemon_cmd.go, while hook generation occurs in internal/git/hook.go.

Frequently Asked Questions

What happens if the post-receive hook passes a relative path like "."?

The normalizeNotifyGatePath function detects that the path is not absolute and calls filepath.Abs to resolve it against the current working directory. This converts the single dot into a full filesystem path before proceeding to symlink resolution and gate validation.

After converting the path to absolute, the function invokes filepath.EvalSymlinks to resolve and collapse any symbolic links in the path chain. This ensures the daemon operates on the physical directory location rather than a potentially malicious symlink pointing outside the authorized gate directory.

Why does the daemon need a git fallback mechanism?

The fallback addresses legacy installations where hooks might not provide the --gate flag or where the path fails validation. By executing git rev-parse --show-toplevel, the daemon can determine the repository root automatically, maintaining backward compatibility with older versions that relied on core.hookspath configurations as documented in issue #269.

Which source files contain the gate path normalization logic?

The primary normalization function normalizeNotifyGatePath is defined in internal/cli/daemon_cmd.go. Unit tests verifying absolute-path enforcement and fallback behavior reside in internal/cli/daemon_cmd_test.go. The hook generation code that embeds the initial gate path is located in internal/git/hook.go.

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 →