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

> Discover how no-mistakes normalizes the post-receive hook gate path. Learn the five-step validation pipeline that resolves symlinks and ensures a canonical location.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: internals
- Published: 2026-07-25

---

**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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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.

### Step 3: Symlink Resolution

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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd.go) illustrates the complete normalization logic:

```go
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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go) during installation. The template embeds the gate path as an environment variable `NM_GATE`:

```sh
#!/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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd.go), while hook generation occurs in [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/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.

### How does no-mistakes prevent symlink attacks on the gate path?

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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd.go). Unit tests verifying absolute-path enforcement and fallback behavior reside in [`internal/cli/daemon_cmd_test.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd_test.go). The hook generation code that embeds the initial gate path is located in [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go).