# Edge Cases for Post-Receive Hook Gate Path Resolution in no-mistakes

> Discover edge cases for post-receive hook gate path resolution in no-mistakes. Learn how its layered absolute-path strategy prevents pipeline failures from relative paths.

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

---

**The no-mistakes CLI implements a layered absolute-path resolution strategy in the post-receive hook and daemon-side normalization to prevent pipeline failures caused by relative paths like "." being passed as the gate directory.**

The `no-mistakes` project uses a bare Git repository as a "gate" that receives pushes to trigger CI pipelines. When Git invokes the `post-receive` hook, the daemon must receive the absolute path of the bare repo via the `--gate` flag; if the hook supplies a relative path, the daemon rejects the push and the pipeline aborts before starting. Robust **post-receive hook gate path resolution** is therefore essential for reliable operation across different Git versions and shell environments.

## How Post-Receive Hook Gate Path Resolution Works

The implementation in [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go) generates a shell script that employs a three-tier fallback strategy to guarantee an absolute path regardless of how Git invokes the hook.

### Step 1: Prefer Git's Absolute Directory

The script first attempts to use Git's built-in capability to report its absolute path. On Git 2.13+ (May 2017), `git rev-parse --absolute-git-dir` returns an absolute path directly.

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

```

This is the primary resolution method found in the `postReceiveHookScript` function within [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go) (lines 39-45).

### Step 2: Fallback to Hook File Location

If the Git command fails—common in older versions or unusual environments—the script derives the repository root from the hook's own location (`$0`). It extracts the hook's directory, climbs one level up to reach the bare repo root, and resolves the physical path using `pwd -P`.

```bash
case "$GATE_DIR" in
  /*) ;;               # already absolute

  *)
    HOOK_PATH=$0
    case "$HOOK_PATH" in
      */*) HOOK_DIR=${HOOK_PATH%/*} ;;
      *)   HOOK_DIR=. ;;
    esac
    GATE_DIR=$(cd "$HOOK_DIR/.." 2>/dev/null && (/bin/pwd -P 2>/dev/null || pwd -P) || :)
    ;;
esac

```

This fallback logic appears in [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go) (lines 46-55).

### Step 3: Final Absolute Path Enforcement

After the fallback, the script double-checks that `GATE_DIR` starts with `/`. If not, it normalizes again using `/bin/pwd -P` or plain `pwd` to ensure the result is absolute before proceeding.

```bash
case "$GATE_DIR" in
  /*) ;;               # already absolute

  *) GATE_DIR=$(/bin/pwd -P 2>/dev/null || pwd -P 2>/dev/null || pwd) ;;
esac

```

This final enforcement step is defined in [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go) (lines 56-59).

## Daemon-Side Gate Path Normalization

Even with hook-side resolution, the daemon command `notify-push` performs additional validation. The `normalizeNotifyGatePath` function in [`internal/cli/daemon_cmd.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd.go) (lines 104-112) guards against malformed input by rejecting empty strings, resolving relative paths with `filepath.Abs`, and cleaning the result with `filepath.Clean`.

```go
func normalizeNotifyGatePath(gate string) (string, error) {
    if strings.TrimSpace(gate) == "" {
        return "", fmt.Errorf("gate path is required")
    }
    abs, err := filepath.Abs(gate)
    if err != nil {
        return "", fmt.Errorf("resolve gate path: %w", err)
    }
    return filepath.Clean(abs), nil
}

```

## Critical Edge Cases Addressed

The dual-layer defense resolves several specific failure modes documented in the codebase and tested in [`internal/cli/daemon_cmd_test.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd_test.go).

### Git Invokes Hook from cwd="." (Issue #269)

**The problem:** When Git invokes the hook from a working directory that collapses to `.`, an unprotected script would forward `--gate .`, which the daemon treats as an invalid gate path, aborting the pipeline.

**The solution:** The script resolves `GATE_DIR` via `git rev-parse` or the hook's physical location, guaranteeing an absolute path regardless of the shell's current directory.

### Legacy Git Versions Without `--absolute-git-dir`

**The problem:** Git versions older than 2.13 lack the `--absolute-git-dir` flag, leaving `GATE_DIR` empty after the first attempt.

**The solution:** The fallback mechanism using `$0` and `pwd -P` guarantees a usable path even when the primary method fails.

### Relative Hook Paths and Symlinked Directories

**The problem:** If `$0` is relative (e.g., just `post-receive`) or the gate directory is symlinked, simple concatenation yields incorrect paths or symlinked paths that mismatch the daemon's lookup.

**The solution:** The script extracts the hook's directory (`HOOK_DIR=${HOOK_PATH%/*}`) and uses `cd` with `pwd -P` to resolve physical paths. The test suite in `TestNormalizeNotifyGatePathResolvesLegacyDotGate` uses `filepath.EvalSymlinks` to verify that normalized paths match the real location (see [`internal/cli/daemon_cmd_test.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd_test.go), lines 33-70).

### Poisoned PWD Environment Variable

**The problem:** Some shells expose the working directory via the `PWD` environment variable; if set to `.` or manipulated, naive scripts might propagate relative paths.

**The solution:** The fallback logic explicitly navigates using `cd` and `pwd -P` rather than trusting environment variables. The unit test explicitly sets `PWD="."` and confirms the result is still absolute, validating resilience against environment variable manipulation.

## Implementation Code Reference

The complete post-receive hook script generated by `PostReceiveHookScript` in [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go) illustrates the full resolution chain:

```bash
#!/bin/sh

# no-mistakes post-receive hook

# Resolve the bare repo dir explicitly. Git can invoke this hook from a cwd

# whose pwd collapses to "." (issue #269), which would pass "--gate ." and be

# rejected by the daemon ("invalid gate path: ."), so the pipeline never

# starts. Prefer git's own absolute dir query (Git 2.13+, May 2017), then fall

# back to the hook file's location so a poisoned PWD still cannot produce ".".

GATE_DIR=$(git rev-parse --absolute-git-dir 2>/dev/null || :)
case "$GATE_DIR" in
  /*) ;;               # already absolute

  *)
    HOOK_PATH=$0
    case "$HOOK_PATH" in
      */*) HOOK_DIR=${HOOK_PATH%/*} ;;
      *)   HOOK_DIR=. ;;
    esac
    GATE_DIR=$(cd "$HOOK_DIR/.." 2>/dev/null && (/bin/pwd -P 2>/dev/null || pwd -P) || :)
    ;;
esac
case "$GATE_DIR" in
  /*) ;;               # already absolute

  *) GATE_DIR=$(/bin/pwd -P 2>/dev/null || pwd -P 2>/dev/null || pwd) ;;
esac

# … proceed to call `no-mistakes daemon notify-push --gate "$GATE_DIR" …`

```

## Summary

- **Absolute path requirement:** The daemon rejects relative paths like `.`, necessitating robust resolution in the post-receive hook.
- **Layered fallback:** The hook prefers `git rev-parse --absolute-git-dir`, falls back to deriving the path from `$0`, and enforces absoluteness with `pwd -P`.
- **Daemon validation:** The `normalizeNotifyGatePath` function in [`internal/cli/daemon_cmd.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd.go) provides a secondary defense, cleaning and validating the path with `filepath.Abs` and `filepath.Clean`.
- **Symlink and environment resilience:** The implementation handles symlinked gate directories and poisoned `PWD` variables by resolving physical paths rather than relying on environment state.
- **Test coverage:** `TestNormalizeNotifyGatePathResolvesLegacyDotGate` explicitly validates the `.` cwd scenario to prevent regression of issue #269.

## Frequently Asked Questions

### What happens if the post-receive hook passes a relative gate path?

The daemon command `notify-push` rejects the push. In [`internal/cli/daemon_cmd.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd.go), the `normalizeNotifyGatePath` function checks if the path is empty or resolves it to an absolute path; if resolution fails or the path remains relative, the daemon returns an error and the pipeline never starts.

### How does no-mistakes handle older Git versions that lack `--absolute-git-dir`?

The hook script generated by [`internal/git/hook.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/git/hook.go) implements a fallback mechanism. If `git rev-parse --absolute-git-dir` fails (common in Git < 2.13), the script derives the repository root from the hook's own location using `$0`, then navigates to the parent directory and uses `pwd -P` to obtain an absolute path.

### Why does the daemon normalize the gate path again if the hook already resolved it?

The daemon-side normalization in `normalizeNotifyGuardPath` serves as a defense-in-depth measure. It protects against any edge cases where the hook might fail or be circumvented, ensuring that `filepath.Clean` removes any `.` or `..` segments and that `filepath.Abs` resolves the path relative to the daemon's actual working directory.

### How are symlinked gate directories handled during path resolution?

The hook script uses `cd` and `pwd -P` (or `/bin/pwd -P`) to resolve physical paths without symlinks. Additionally, the test suite in [`internal/cli/daemon_cmd_test.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/cli/daemon_cmd_test.go) uses `filepath.EvalSymlinks` to verify that the normalized gate path matches the real directory location, ensuring consistency even when the gate path involves symbolic links.