Skip to content

Commit d7dbe21

Browse files
Various bug fixes and hooks (#10346)
1 parent 78e0727 commit d7dbe21

174 files changed

Lines changed: 10138 additions & 1580 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/commands/ralph.md

Lines changed: 447 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env bash
2+
input=$(cat)
3+
command=$(echo "$input" | grep -oP '"command"\s*:\s*"\K[^"]*' | head -1)
4+
5+
if echo "$command" | grep -qP '[A-Za-z]:\\\\'; then
6+
cat <<'EOF'
7+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
8+
"permissionDecisionReason":"Windows backslash paths are mangled by Git Bash. Use forward slashes instead (e.g. C:/github/LeLab/scratchpad/foo.ps1). PowerShell handles forward slashes fine on Windows."}}
9+
EOF
10+
exit 0
11+
fi
12+
13+
exit 0
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
#!/usr/bin/env bash
2+
input=$(cat)
3+
command=$(echo "$input" | grep -oP '"command"\s*:\s*"\K[^"]*' | head -1)
4+
5+
# Block powershell.exe entirely
6+
if echo "$command" | grep -qiP '\bpowershell(\.exe)?\b'; then
7+
cat <<'EOF'
8+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
9+
"permissionDecisionReason":"Do not use powershell.exe (Windows PowerShell 5.1). Write a .ps1 file and run: pwsh -NoProfile -File <script.ps1>"}}
10+
EOF
11+
exit 0
12+
fi
13+
14+
# Block pwsh -Command / pwsh -c
15+
if echo "$command" | grep -qiP 'pwsh(\s+-\w+)*\s+-(c|Command)\b'; then
16+
cat <<'EOF'
17+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
18+
"permissionDecisionReason":"Do not run inline PowerShell via pwsh -Command. Write the script to scratchpad/<name>.ps1 first, then execute with: pwsh -NoProfile -File scratchpad/<name>.ps1"}}
19+
EOF
20+
exit 0
21+
fi
22+
23+
exit 0
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env bash
2+
input=$(cat)
3+
command=$(echo "$input" | grep -oP '"command"\s*:\s*"\K[^"]*' | head -1)
4+
5+
# Block git push --force and variants (-f, --force-with-lease, --force-if-includes)
6+
if echo "$command" | grep -qiP 'git\s+push\s+.*(-f|--force)\b'; then
7+
cat <<'EOF'
8+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
9+
"permissionDecisionReason":"Force push is blocked. Use regular git push instead. If you need to force push, ask the user to do it manually."}}
10+
EOF
11+
exit 0
12+
fi
13+
14+
# Block git reset --hard
15+
if echo "$command" | grep -qiP 'git\s+reset\s+.*--hard\b'; then
16+
cat <<'EOF'
17+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
18+
"permissionDecisionReason":"git reset --hard is blocked because it discards uncommitted changes. Use git stash or git checkout -- <file> instead."}}
19+
EOF
20+
exit 0
21+
fi
22+
23+
# Block git clean -f (force delete untracked files)
24+
if echo "$command" | grep -qiP 'git\s+clean\s+.*-[a-zA-Z]*f'; then
25+
cat <<'EOF'
26+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
27+
"permissionDecisionReason":"git clean -f is blocked because it permanently deletes untracked files. Ask the user to run it manually if needed."}}
28+
EOF
29+
exit 0
30+
fi
31+
32+
# Block git checkout with --force/-f on branches (not file restores)
33+
if echo "$command" | grep -qiP 'git\s+checkout\s+(-f|--force)\b'; then
34+
cat <<'EOF'
35+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
36+
"permissionDecisionReason":"git checkout --force is blocked because it discards local changes. Use git stash first, then checkout."}}
37+
EOF
38+
exit 0
39+
fi
40+
41+
# Block git branch -D (force delete)
42+
if echo "$command" | grep -qiP 'git\s+branch\s+.*-D\b'; then
43+
cat <<'EOF'
44+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
45+
"permissionDecisionReason":"git branch -D (force delete) is blocked. Use git branch -d for safe deletion, or ask the user to force-delete manually."}}
46+
EOF
47+
exit 0
48+
fi
49+
50+
# Block git rebase on shared/remote branches (rebase with upstream refs)
51+
if echo "$command" | grep -qiP 'git\s+rebase\s+.*(origin|upstream)\b'; then
52+
cat <<'EOF'
53+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
54+
"permissionDecisionReason":"Rebasing against remote branches is blocked to prevent history rewrites. Ask the user before rebasing."}}
55+
EOF
56+
exit 0
57+
fi
58+
59+
# Block amending commits (could rewrite published history)
60+
if echo "$command" | grep -qiP 'git\s+commit\s+.*--amend\b'; then
61+
cat <<'EOF'
62+
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny",
63+
"permissionDecisionReason":"git commit --amend is blocked because it rewrites commit history. Create a new commit instead, or ask the user to amend manually."}}
64+
EOF
65+
exit 0
66+
fi
67+
68+
exit 0

.claude/hooks/redirect-glob.sh

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env bash
2+
input=$(cat)
3+
tool=$(echo "$input" | grep -oP '"tool_name"\s*:\s*"\K[^"]*' | head -1)
4+
5+
if [ "$tool" != "Glob" ]; then
6+
exit 0
7+
fi
8+
9+
pattern=$(echo "$input" | grep -oP '"pattern"\s*:\s*"\K[^"]*' | head -1)
10+
path=$(echo "$input" | grep -oP '"path"\s*:\s*"\K[^"]*' | head -1)
11+
12+
search_in=""
13+
if [ -n "$path" ]; then
14+
search_in=" \"$path\""
15+
fi
16+
17+
cat >&2 <<EOF
18+
BLOCKED: Glob tool is unreliable on Windows (timeouts, silent failures).
19+
Use fd via Bash instead. fd respects .gitignore and is much faster.
20+
21+
Your pattern was: $pattern
22+
Equivalent fd commands:
23+
/c/ProgramData/chocolatey/bin/fd.exe --type f --glob '$pattern'$search_in
24+
/c/ProgramData/chocolatey/bin/fd.exe --type f --extension ps1$search_in
25+
/c/ProgramData/chocolatey/bin/fd.exe --type f 'keyword'$search_in
26+
EOF
27+
exit 2

.claude/hooks/stop-todo-report.sh

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/bin/bash
2+
# stop-todo-report.sh - Scan changed files for TODO/FIXME and report with header
3+
# Runs at Stop to surface any incomplete work left in the code.
4+
5+
INPUT=$(cat)
6+
7+
# Prevent re-entry if stop hook is already active
8+
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
9+
if [[ "$STOP_ACTIVE" == "true" ]]; then
10+
exit 0
11+
fi
12+
13+
# Get all modified/added files from git (staged + unstaged + untracked)
14+
CHANGED_FILES=$(git diff --name-only HEAD 2>/dev/null; git diff --cached --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null)
15+
CHANGED_FILES=$(echo "$CHANGED_FILES" | sort -u | grep -E '\.(ps1|psm1|psd1|cs|sql|js|ts|html|go|py|sh)$')
16+
17+
if [[ -z "$CHANGED_FILES" ]]; then
18+
exit 0
19+
fi
20+
21+
# Scan for TODO/FIXME/HACK/XXX/WORKAROUND in changed files
22+
TODO_REPORT=""
23+
while IFS= read -r file; do
24+
[[ -f "$file" ]] || continue
25+
HITS=$(grep -n -i -E '\b(TODO|FIXME|HACK|XXX|WORKAROUND)\b' "$file" 2>/dev/null)
26+
if [[ -n "$HITS" ]]; then
27+
TODO_REPORT+="### $file\n"
28+
while IFS= read -r line; do
29+
TODO_REPORT+=" $line\n"
30+
done <<< "$HITS"
31+
TODO_REPORT+="\n"
32+
fi
33+
done <<< "$CHANGED_FILES"
34+
35+
if [[ -n "$TODO_REPORT" ]]; then
36+
# Escape for JSON
37+
ESCAPED=$(echo -e "$TODO_REPORT" | jq -Rs .)
38+
jq -n --argjson report "$ESCAPED" '{
39+
decision: "block",
40+
reason: ("⚠️ UNFINISHED WORK DETECTED — do not stop until resolved.\n\nThe following TODO/FIXME/HACK items were found in changed files.\nFor each one you MUST either:\n\n 1. Resolve it now (implement the missing code), OR\n\n 2. If you cannot finish due to context window size or complexity, write a self-contained prompt to docs/prompts/ that a fresh Claude session can run to complete the work. The prompt MUST:\n - Describe exactly what each TODO requires\n - Include all relevant file paths and line numbers\n - Use the Agent tool with specialized subagents where appropriate (e.g. psu-developer, hugo-frontend, csharp-engineer)\n - End with an instruction to commit the completed work using conventional commits\n Then tell the user: \"I wrote a completion prompt to docs/prompts/<filename>.md — run it in a new session to finish.\"\n\n 3. As a last resort only: explicitly tell the user what remains and why it cannot be done at all.\n\nDo NOT silently leave TODOs behind. Go finish them.\n\n" + $report + "\n--- End of TODO Report ---")
41+
}'
42+
fi
43+
44+
exit 0

.claude/hooks/stop-verify.sh

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/bin/bash
2+
# stop-verify.sh - Comprehensive quality gate when Claude finishes responding
3+
# Incorporates: doublecheck, simplify, review, verify, and completeness checks.
4+
# Injects a reminder to self-verify. Does NOT hard-block (no infinite loops).
5+
# Guards against re-entry via stop_hook_active flag.
6+
7+
INPUT=$(cat)
8+
9+
# Prevent infinite loops: if stop hook is already active, exit immediately
10+
STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
11+
if [[ "$STOP_ACTIVE" == "true" ]]; then
12+
exit 0
13+
fi
14+
15+
jq -n '{
16+
hookSpecificOutput: {
17+
hookEventName: "Stop",
18+
additionalContext: "QUALITY GATE — If you wrote or modified code in this response, perform ALL checks below before finishing. If you only answered a question or did research, skip this.\n\n## 1. VERIFY (does it work?)\n- Check for syntax errors in changed files\n- Run tests if applicable (Pester for PS, dotnet test for C#)\n- Confirm imports, dependencies, and module loading work\n- Report what works and what does not\n\n## 2. REVIEW (is it safe and correct?)\n- Logic errors and edge cases\n- Security: credential handling, injection vulnerabilities (SQL, XSS)\n- Missing validation on user input\n- Error responses must not expose stack traces\n- Destructive operations need confirmation\n- API naming contract (plural URLs, kebab-case)\n- PSU endpoints use New-ProtectedEndpoint\n\n## 3. SIMPLIFY (is it clean?)\n- Remove unnecessary complexity\n- Consolidate duplicate logic\n- Use idiomatic patterns (PowerShell best practices for .ps1)\n- Remove dead code, unused variables, unused imports\n- Do not add features or change functionality — only simplify\n\n## 4. DOUBLECHECK (final verification)\nCreate a table:\n| Claim/Item | Verified? | Notes |\n|------------|-----------|-------|\nInclude: primary functionality, tests passing, security, naming conventions.\nBe thorough and honest about what you could not verify.\n\n## 5. COMPLETENESS\n- Were ALL requested changes made?\n- Any TODO/FIXME left behind that should be resolved?\n- Files over 400 lines that need splitting?\n- OBSERVABILITY IN ACTION: If you built or modified a page displaying fleet data, does it include action capabilities (Fix Now / Schedule / Execute buttons)? Display-only pages are not acceptable unless purely audit/history.\n\nIf anything fails, fix it before finishing."
19+
}
20+
}'
21+
22+
exit 0

.claude/hooks/validate-style.ps1

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
#!/usr/bin/env pwsh
2+
# PreToolUse hook: Consolidated style validation for dbatools
3+
# Runs all style checks in a single PowerShell process for performance
4+
5+
$ErrorActionPreference = "Stop"
6+
7+
try {
8+
$inputJson = $input | Out-String | ConvertFrom-Json
9+
} catch {
10+
exit 0
11+
}
12+
13+
$toolInput = $inputJson.tool_input
14+
$filePath = $toolInput.file_path
15+
if (-not $filePath) { exit 0 }
16+
if ($filePath -notlike "*.ps1") { exit 0 }
17+
18+
$content = if ($toolInput.new_string) { $toolInput.new_string } else { $toolInput.content }
19+
if (-not $content) { exit 0 }
20+
21+
$violations = @()
22+
$lines = $content -split "`n"
23+
24+
# Track state for multi-line constructs
25+
$inHereStringSingle = $false
26+
$inHereStringDouble = $false
27+
$inHashtable = $false
28+
$hashtableLines = @()
29+
$hashtableStart = 0
30+
$misalignedHashtables = @()
31+
32+
# Patterns (using double quotes with escaping)
33+
$patternComment = "^\s*#"
34+
$patternHereStringSingleStart = "@'"
35+
$patternHereStringDoubleStart = "@`""
36+
$patternHereStringSingleEnd = "^'@"
37+
$patternHereStringDoubleEnd = "^`"@"
38+
$patternBacktick = "``\s*$"
39+
$patternBoolAttribute = "\[\s*(Parameter|CmdletBinding|OutputType|ValidateSet)\s*\([^]]*=\s*\`$(true|false)"
40+
$patternStaticNew = "::new\s*\("
41+
$patternSingleQuote = "(?<![@])'.+'"
42+
$patternPlainSplat = "\`$splat\s*="
43+
$patternNamedSplat = "\`$splat[A-Z][a-zA-Z0-9]*\s*="
44+
$patternArrayList = "ArrayList"
45+
$patternGenericList = "Generic\.List"
46+
$patternStandaloneBrace = "^\s*\{\s*$"
47+
$patternPrevLineEnd = "\)\s*$"
48+
$patternControlKeyword = "(if|else|elseif|foreach|for|while|switch|try|catch|finally)\s*$"
49+
$patternHashtableStart = "@\{"
50+
$patternHashtableEnd = "^\s*\}"
51+
$patternTrailingSpace = "\s+$"
52+
53+
for ($i = 0; $i -lt $lines.Count; $i++) {
54+
$line = $lines[$i].TrimEnd("`r")
55+
$lineNum = $i + 1
56+
$isComment = $line -match $patternComment
57+
58+
# Track here-string state
59+
if ($line -match $patternHereStringSingleStart) { $inHereStringSingle = $true }
60+
if ($line -match $patternHereStringDoubleStart) { $inHereStringDouble = $true }
61+
if ($line -match $patternHereStringSingleEnd) { $inHereStringSingle = $false; continue }
62+
if ($line -match $patternHereStringDoubleEnd) { $inHereStringDouble = $false; continue }
63+
if ($inHereStringSingle -or $inHereStringDouble) { continue }
64+
65+
# Skip comments for most checks
66+
if (-not $isComment) {
67+
# 1. No backticks for line continuation
68+
if ($line -match $patternBacktick) {
69+
$violations += "Line ${lineNum}: Backtick line continuation. Use splatting instead."
70+
}
71+
72+
# 2. No = $true in parameter attributes
73+
if ($line -match $patternBoolAttribute) {
74+
$violations += "Line ${lineNum}: Use [Parameter(Mandatory)] not = `$true syntax."
75+
}
76+
77+
# 3. No static new method (PowerShell v3 compatibility)
78+
if ($line -match $patternStaticNew) {
79+
$violations += "Line ${lineNum}: Use New-Object for PS v3 compatibility."
80+
}
81+
82+
# 4. No single quotes (except here-strings already handled above)
83+
if ($line -match $patternSingleQuote) {
84+
$violations += "Line ${lineNum}: Use double quotes instead of single quotes."
85+
}
86+
87+
# 5. No plain $splat variable names
88+
if ($line -match $patternPlainSplat -and $line -notmatch $patternNamedSplat) {
89+
$violations += "Line ${lineNum}: Use `$splat<Purpose> naming (e.g., `$splatConnection)."
90+
}
91+
92+
# 6. No ArrayList or Generic.List collection
93+
if ($line -match $patternArrayList) {
94+
$violations += "Line ${lineNum}: Output directly to pipeline, not ArrayList."
95+
}
96+
if ($line -match $patternGenericList) {
97+
$violations += "Line ${lineNum}: Output directly to pipeline, not Generic.List."
98+
}
99+
100+
# 7. OTBS - no standalone opening brace (Allman style)
101+
if ($line -match $patternStandaloneBrace -and $i -gt 0) {
102+
$prevLine = $lines[$i - 1].TrimEnd("`r")
103+
if ($prevLine -match $patternPrevLineEnd -or $prevLine -match $patternControlKeyword) {
104+
$violations += "Line ${lineNum}: Use OTBS - opening brace on same line as statement."
105+
}
106+
}
107+
108+
# 8. Track hashtables for alignment check
109+
if ($line -match $patternHashtableStart) {
110+
$inHashtable = $true
111+
$hashtableLines = @()
112+
$hashtableStart = $lineNum
113+
# Don't add this line to hashtableLines - it's the opening, not an entry
114+
} elseif ($inHashtable) {
115+
if ($line -match $patternHashtableEnd) {
116+
if ($hashtableLines.Count -ge 2) {
117+
$equalsPositions = $hashtableLines | ForEach-Object {
118+
$pos = $_.IndexOf("=")
119+
if ($pos -ge 0) { $pos }
120+
} | Where-Object { $null -ne $_ } | Select-Object -Unique
121+
if ($equalsPositions.Count -gt 1) {
122+
$misalignedHashtables += "Lines $hashtableStart-$lineNum"
123+
}
124+
}
125+
$inHashtable = $false
126+
} elseif ($line -match "=" -and $line.Trim() -ne "") {
127+
$hashtableLines += $line
128+
}
129+
}
130+
}
131+
132+
# 9. No trailing spaces (check even in comments)
133+
if ($line -match $patternTrailingSpace) {
134+
$violations += "Line ${lineNum}: Trailing whitespace."
135+
}
136+
}
137+
138+
# Add hashtable alignment violations
139+
foreach ($ht in $misalignedHashtables) {
140+
$violations += "${ht}: Hashtable = signs must be vertically aligned."
141+
}
142+
143+
if ($violations.Count -gt 0) {
144+
$summary = ($violations | Select-Object -First 5) -join "`n"
145+
$more = if ($violations.Count -gt 5) { "`n... and $($violations.Count - 5) more violations" } else { "" }
146+
[Console]::Error.WriteLine("BLOCKED: dbatools style violations:`n$summary$more")
147+
exit 2
148+
}
149+
150+
exit 0

.claude/settings.json

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"hooks": {
3+
"PreToolUse": [
4+
{
5+
"matcher": "Edit|Write",
6+
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "pwsh -NoProfile -File .claude/hooks/validate-style.ps1"
10+
}
11+
]
12+
},
13+
{
14+
"matcher": "Bash",
15+
"hooks": [
16+
{
17+
"type": "command",
18+
"command": "bash .claude/hooks/prevent-destructive-git.sh"
19+
}
20+
]
21+
}
22+
]
23+
}
24+
}

.gitignore

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,12 @@ allcommands.ps1
5757
/.aitools/.aitools/.aider
5858
/.aitools
5959
.aider*
60-
/.shadowgit.git
61-
/.claude
62-
publish.ps1
60+
/.shadowgit.git
61+
publish.ps1
6362

6463
PR_BODY.md
6564

6665
PR_DESCRIPTION.md
67-
nul
66+
nul
67+
.claude/settings.local.json
68+
/scripts

0 commit comments

Comments
 (0)