Add optional pre-commit hook for code formatting #5178
7 issues
High
`set -e` causes script to exit before formatting failure path runs, leaving stash unrestored on dotnet format errors - `.githooks/pre-commit:29-30`
The script uses set -e combined with git stash push --keep-index. If dotnet format exits non-zero (e.g., build failure, restore issue, or tooling error), set -e aborts the script immediately. While the EXIT trap will still fire and attempt to restore the stash, the script never reaches the diff check or the user-facing error message — developers get a silent/cryptic failure with output redirected to /dev/null 2>&1, making it impossible to diagnose why the hook failed. Combined with 2>&1 > /dev/null, real formatter errors are completely swallowed.
dotnet format mutates the working tree, leaving format edits behind on failure and corrupting stash pop - `.githooks/pre-commit:28-29`
The hook runs dotnet format without --verify-no-changes, so it actively rewrites files in the working tree instead of just checking them (contradicting the PR's stated 'check-only mode'). When formatting issues exist, the script exits 1 and the EXIT trap runs git stash pop, but the working tree now contains dotnet format's edits on top of the staged content. Restoring the previously stashed unstaged changes will frequently conflict, and even when it doesn't, the developer is left with unrequested formatting modifications applied to their working tree (and possibly their stash dropped). On a successful commit the hook also leaves these working-tree mutations in place, since nothing resets them before exit.
Medium
Unquoted `./**/*OptionsSetup.cs` glob is expanded by bash, not passed to dotnet format - `.githooks/pre-commit:30`
The --exclude argument ./**/*OptionsSetup.cs is unquoted, so bash expands it before passing to dotnet format. Without shopt -s globstar, ** behaves as a single * and won't recurse; with globstar enabled it expands to a list of paths that are passed as multiple arguments. Either way, the behavior diverges from CI (which presumably passes the literal pattern) and the exclude may not match the intended files, causing the hook to fail commits over files that CI ignores.
`dotnet format` runs against entire working tree, not just staged changes, so unrelated formatting issues block commits - `.githooks/pre-commit:26-34`
git stash push --keep-index stashes unstaged changes, leaving staged changes in the working tree — but the working tree still contains all other tracked files. dotnet format then scans the entire solution and may reformat files that the developer didn't touch (e.g., pre-existing formatting drift on unrelated files). The hook will then fail the commit even though the staged diff is clean, blocking commits for issues outside the developer's change scope.
`set -e` causes the hook to abort silently if dotnet format exits non-zero for any reason - `.githooks/pre-commit:28-29`
With set -e enabled at the top of the script, any non-zero exit from dotnet format (e.g., build/restore error, invalid argument, dotnet not installed) will terminate the script immediately. The EXIT trap then runs git stash pop and the script exits non-zero, blocking the commit without printing the helpful 'formatting issues found' message — and without surfacing the actual dotnet error, since stdout/stderr are redirected to /dev/null. Developers will see a failed commit with no diagnostic output.
`./**/*OptionsSetup.cs` is expanded by bash before reaching dotnet format, producing wrong --exclude arguments - `.githooks/pre-commit:29`
The script passes ./**/*OptionsSetup.cs unquoted to dotnet format. Bash will perform pathname expansion on this token before invoking dotnet. Without shopt -s globstar, ** behaves like * and matches only one directory level; with globstar enabled it expands recursively. Either way, dotnet format receives a list of concrete paths (or the literal pattern if no match) — not the intended glob. As a result the exclude list differs from CI, so the hook may flag (or miss) files that CI treats differently, defeating the 'matches CI command exactly' goal.
Low
Setup script does not ensure pre-commit hook is executable - `scripts/setup-hooks.sh:7`
The setup script configures core.hooksPath to .githooks but does not chmod +x .githooks/pre-commit. If the file is checked out without the executable bit (e.g., on systems where git's filemode tracking is disabled, or if the file was committed without it), git will silently skip the hook, defeating the formatting check. Developers may believe formatting is being verified when it is not.
4 skills analyzed
| Skill | Findings | Duration | Cost |
|---|---|---|---|
| code-review | 3 | 38.3s | $0.56 |
| find-bugs | 4 | 59.0s | $1.30 |
| gha-security-review | 0 | 18.0s | $0.30 |
| security-review | 0 | 24.4s | $0.34 |
Duration: 2m 20s · Tokens: 237.5k in / 6.6k out · Cost: $2.52 (+merge: $0.00, +consolidate: $0.00, +dedup: $0.01)