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.
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 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.Absto eliminate ambiguity from bare repository invocations. - Symlink protection:
filepath.EvalSymlinkscollapses symbolic links to prevent directory traversal outside the intended gate. - Structure validation: The
isGateDircheck ensures the path contains the requiredhooks/post-receivescript and internal database. - Legacy support: The git fallback mechanism uses
git rev-parse --show-toplevelto support older installations referenced in issue #269. - Key files: The normalization logic lives in
internal/cli/daemon_cmd.go, while hook generation occurs ininternal/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. 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →