feat: add powershell-format plugin#39
Conversation
Migrate medley's powershell-format hook into a repo-agnostic marketplace plugin. Mirrors the ruff-format/biome-format tool-runner pattern: - Opt-in on a consumer PSScriptAnalyzerSettings.psd1, discovered by walking up from the edited file to the repo root (keeps the medley seam, adds monorepo-safe per-module discovery). - Graceful degrade: pwsh absent OR PSScriptAnalyzer module absent -> clean silent no-op (exit 3 sentinel from the pwsh probe; exit 4 = analyzer throw surfaces as advisory tool-break). - Advisory only (always exit 0); kill switch HOOK_POWERSHELL_FORMAT_ENABLED; opt-in telemetry envelope via HOOK_TELEMETRY_SINK. - Byte-identical hook-utils.sh copy (sync-hook-utils SSOT). Fixes a latent bug inherited from the medley baseline: PowerShell string -ne is case-insensitive, so a casing-only reformat (PSUseCorrectCasing) never persisted. The formatted output is now compared with case-sensitive -cne. Self-contained contract tests (36 cases; skip cleanly without pwsh, with deterministic pwsh-stub cases for the exit-3/exit-4 degrade paths).
…ath analyzer, document settings trust model Security-review follow-up (ACCEPT-WITH-NOTES): - Bound the PSScriptAnalyzerSettings.psd1 walk-up ceiling by CLAUDE_PROJECT_DIR when set, so the settings ceiling matches the file-membership ceiling hook::read_file_path already enforces; the git-root ceiling stays the fallback when unset. A settings file above the project dir can no longer govern an edit. - Analyzer call: Invoke-ScriptAnalyzer has no -LiteralPath (verified against module 1.25.0 — only -Path, which treats [ ] * ? as wildcards). Realize the literal-path intent via [WildcardPattern]::Escape($file) on -Path; a filename containing wildcard metacharacters was otherwise silently skipped (verified: raw -Path on weird[1].ps1 -> 0 findings; escaped -> 1). - README: document the settings-file trust model. A PSScriptAnalyzerSettings.psd1 may declare CustomRulePath modules that PSScriptAnalyzer loads and executes during analysis (Microsoft Learn, "Using PSScriptAnalyzer > Custom rules"), so it is executed-adjacent config — trust it as you trust build/CI config. Contract suite: 39 cases green (adds ceiling-bound + git-root-fallback contrast).
️✅ There are no secrets present in this pull request anymore.If these secrets were true positive and are still valid, we highly recommend you to revoke them. 🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_c3929678-0377-4221-9d1c-8ea7a7ba8ffa) |
|
Claude finished @kyle-sexton's task in 3m 26s —— View job PR Review: feat/plugin-powershell-format
|
| Surface | Status | Notes |
|---|---|---|
| Code execution | ✅ Pass | Advisory (always exits 0), no eval/curl | sh, kill switch present, file path flows through jq parse then hook::read_file_path membership guard before use |
| Untrusted input in shell | ✅ Pass | File and settings paths passed to pwsh via env vars (L164–165), not as CLI arguments — no word-splitting or glob expansion risk |
| Remote MCP | ✅ N/A | No MCP server |
userConfig secrets |
✅ N/A | No userConfig (intentional; PSScriptAnalyzerSettings.psd1 is the config surface) |
| Cache isolation | ✅ Pass | ${CLAUDE_PLUGIN_ROOT} in hooks.json command; no ../ reach-outs |
| Data egress / telemetry | ✅ Pass | HOOK_TELEMETRY_SINK is opt-in; unset = exact no-op; fire-and-forget to a local executable only; no outbound network |
| Provenance | ✅ Pass | Author matches plugin.json; MIT license; no third-party SaaS wiring |
The WildcardPattern::Escape workaround for Invoke-ScriptAnalyzer -Path (L192–193) is the right call — filenames with [, ], *, or ? would be treated as glob patterns by -Path without it. Good defensive code, and the comment documents the module version it was verified against.
Correctness
-cne fix is load-bearing. The comment at L178–180 explains it clearly: PowerShell's -ne is case-insensitive, so get-childitem vs Get-ChildItem compare as equal and the formatter write-back would silently no-op. The fix to -cne is correct, necessary, and the test suite proves it (Case 3 in the test file uses PSUseCorrectCasing specifically to exercise this path).
Settings walk ceiling (L111–133) is properly bounded: when CLAUDE_PROJECT_DIR is set, the walk stops at the project dir rather than the git root, matching the file-membership guard in hook::read_file_path. The contrast test (Case 1b in the test file) proves the ceiling is active by confirming a root-level settings file IS found when CLAUDE_PROJECT_DIR is unset.
PSSA_OUTPUT merges stdout+stderr (L207 2>&1). This is intentional — the pwsh block routes findings to Console.Error.WriteLine and all diagnostic text should feed the advisory context. The merge is sound but a brief inline note clarifying the intent (that stderr findings are intentionally captured) would help the next reader. Minor.
Telemetry status semantics: Status "ok" on the exit-1 (findings) path (L236) means "the analyzer ran to judgment" — consistent with sibling plugins. The findings themselves live in data.findings, so a telemetry sink can distinguish clean vs violations without relying on status. This is well-aligned with the playbook's reintegration guidance on the observability contract.
Plugin Manifest & Structure
plugin.json is valid: $schema, name, version (0.1.0), description, author, keywords all present. No userConfig is correct given the consumer-owned settings file is the config surface (documented clearly in README).
hooks.json uses "${CLAUDE_PLUGIN_ROOT}"/hooks/powershell-format.sh — correct cache-isolation pattern, consistent with all other plugins in this marketplace.
hook-utils.sh is byte-identical to lib/hook-utils.sh (confirmed via diff). The SSOT copy is clean. The double-source guard at line 12 prevents re-sourcing issues.
The marketplace entry (powershell-format, category formatting, tags matching plugin.json keywords) is clean.
Test Coverage
The 39-test suite is the strongest aspect of this PR. Highlights worth noting:
- Stub pwsh (exit 3 / exit 4) makes the module-absent and tool-break paths deterministic — tests run correctly even on runners without PSScriptAnalyzer installed.
- Ceiling contrast (Case 1b) proves the ceiling was the active mechanism, not a missing file.
- Telemetry envelope validation checks all 7 required fields, schema version,
data.fileis repo-relative, andduration_msis a non-negative integer. - pwsh-absent PATH surgery is clever; the fallback
echo "SKIP: ..."handles the case where pwsh shares a directory with jq/git gracefully.
One minor note: wait_for_sink polls at 20ms × 150 tries (3 s max) for the fire-and-forget background subshell. On a heavily loaded CI runner this is usually fine but could be flaky in extreme cases. Pre-existing pattern from sibling plugins, acceptable as-is.
Minor / Nits
-
_cpdrename (optional): Renaming to_canonical_cpdat L113–114 would make the GitGuardian false positive less likely to reoccur on future scans. Not required for correctness. Fix this → -
2>&1intent comment (optional): A one-line note above L2072>&1explaining that stderr findings are intentionally captured would help future maintainers understand why stderr is merged rather than discarded. -
Trust model section in README: The
CustomRulePathwarning is appropriately placed and well-worded. This is the right level of disclosure for an "executed-adjacent configuration" surface.
Verdict: Approve. Close the GitGuardian false positive, optionally rename _cpd, then merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e56081660d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…at settings dir Address Codex review findings on #39: - P2: read with BOM detection (StreamReader) and write back with the detected encoding — pwsh's Set-Content default (utf8NoBOM) stripped a UTF-8 BOM and transcoded UTF-16 on first format. Files that are neither BOM'd nor valid UTF-8 (legacy ANSI) skip cleanly (exit 5) instead of being transcoded. - P2: Set-Location to the settings directory before Invoke-Formatter / Invoke-ScriptAnalyzer so repo-relative CustomRulePath entries resolve from the settings file, not the agent's incidental cwd.
…l-format # Conflicts: # .claude-plugin/marketplace.json # README.md
PR #38 added the marketplace.json entry but not the root README catalog row; every other plugin PR carries both.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_e1532280-0876-410a-827a-685787f22a39) |
|
Claude finished @kyle-sexton's task in 2m 12s —— View job PR Review: feat/plugin-powershell-format
What Changed Since the Previous ReviewCommit Codex P2 #1 — Encoding Preservation ✅ ResolvedFinding: Fix (L187–208):
Tests:
The implementation is correct. The Codex P2 #2 — CustomRulePath Working Directory ✅ ResolvedFinding: If Fix (L179): Set-Location -LiteralPath (Split-Path -Parent $settings)Anchors PowerShell's cwd at the directory containing the settings file before both Test (Case 10): Creates a test repo with Correctness NotesAll correctness observations from the previous review stand and none are invalidated by the new commit:
Minor ObservationThe README doesn't mention encoding preservation behavior or the ANSI-skip exit path. Users with mixed-encoding repos would benefit from a note in the Behavior section that the hook preserves the original file encoding and skips files with non-UTF-8 bytes safely rather than transcoding them. Low priority — these are implementation details that don't affect basic usage, and the hook comment block at the top of the script explains it well. Verdict: ApproveBoth Codex P2 findings are resolved with correct implementation and solid test coverage (Cases 8, 9, 10). The security posture, graceful degrade, |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 57014d3252
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…fixture The escaped 'caf\xE9' left a bare 'caf' token that trips the typos CI gate; the lone high byte is all the fixture needs.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_90e5b553-c356-4033-a440-e6b0bbeecec6) |
There was a problem hiding this comment.
Stale comment
Risk: medium. Cursor Bugbot skipped (usage limit reached), so automated review results are incomplete and I did not approve. Human review is needed; no reviewers were assigned because no non-author reviewers are available in this repository.
Sent by Cursor Approval Agent: Pull Request Router and Approver
Use hook::physical_path for FILE_DIR_POSIX and CEILING so symlinked CLAUDE_PROJECT_DIR matches the resolved file directory during the PSScriptAnalyzerSettings.psd1 walk-up. Co-Authored-By: Claude <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_44e88bbd-cf05-4e01-a9b4-92389ac35c4a) |
|
Claude finished @kyle-sexton's task in 2m 15s —— View job PR Review: feat/plugin-powershell-format
What Changed Since the Previous ReviewTwo commits since the last approved round:
Codex P2 #3 — Symlink Ceiling Normalization ✅ ResolvedFinding: When Fix (L104–116): Before: FILE_DIR_POSIX="$(cd "$(dirname "$FILE")" 2>/dev/null && pwd)" || FILE_DIR_POSIX=""
root="$(cd "$REPO_ROOT" 2>/dev/null && pwd)" || root=""
# …
_cpd="$(cd "$CLAUDE_PROJECT_DIR" 2>/dev/null && pwd)" || _cpd=""
[[ -n "$_cpd" ]] && CEILING="$_cpd"After: FILE_DIR_POSIX="$(hook::normalize_path "$(hook::physical_path "$(dirname "$FILE")")")" || FILE_DIR_POSIX=""
root="$(hook::normalize_path "$(hook::physical_path "$REPO_ROOT")")" || root=""
# …
CEILING="$(hook::normalize_path "$(hook::physical_path "$CLAUDE_PROJECT_DIR")")"This is exactly correct. Incidentally, this also eliminates the One subtle change: the old code guarded ANSI Fixture Cleanup ✅ CorrectCommit # Before
printf 'write-output "caf\xE9"\n' >"$REPO/ansi.ps1"
# After
printf 'write-output "\xE9"\n' >"$REPO/ansi.ps1"The All Prior Findings Still Hold
Minor ObservationThere is no dedicated test for the symlinked Verdict: Approve. All three Codex P2 findings are resolved with correct implementations and the typos CI fix is clean. The hook is ready to merge. |
There was a problem hiding this comment.
Stale comment
Risk: medium. Cursor Bugbot skipped (usage limit reached), so automated review results are incomplete and I did not approve. Human review is needed; no reviewers were assigned because no non-author reviewers are available in this repository.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1c90d741ac
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The hook probes pwsh only; claiming Windows PowerShell support was inaccurate. PSScriptAnalyzer behavior also differs on 5.1, so the supported engine is stated exactly.
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_733c79bb-7622-4494-a942-4713b370418b) |
|
Claude finished @kyle-sexton's task in 1m 13s —— View job PR Review: feat/plugin-powershell-format
What Changed Since the Previous ReviewOne commit since the last approved round:
Only Codex P2 #4 — Windows PowerShell Scope ✅ Resolved (docs-only fix, correct approach)Finding: The README previously claimed support for "PowerShell 7+ (cross-platform) or Windows PowerShell". The hook only probes Fix (L49–51): -**PowerShell** (`pwsh`) on `PATH` — PowerShell 7+ (cross-platform) or Windows
- PowerShell. If absent, the hook is a silent no-op.
+**PowerShell 7+** (`pwsh`) on `PATH` — the hook probes `pwsh` only; legacy
+ Windows PowerShell 5.1 (`powershell.exe`) is not used. If absent, the hook is
+ a silent no-op.This is the right fix. The alternative (falling back to All Prior Findings Still HoldAll correctness, security, and structural conclusions from the three previous review cycles are unchanged — this commit touched only README prose, not hook logic:
Verdict: ApproveAll four Codex P2 findings across the review cycle are now resolved:
The hook is well-engineered, security-reviewed, and ships a thorough contract test suite. Ready to merge. |
There was a problem hiding this comment.
Risk: medium. Cursor Bugbot skipped (usage limit reached), so automated review results are incomplete and I did not approve. Human review is needed; no reviewers were assigned because no non-author reviewers are available in this repository.
Sent by Cursor Approval Agent: Pull Request Router and Approver



Migrates the PowerShell (PSScriptAnalyzer) formatter hook into a repo-agnostic plugin. Consumer conventions from PSScriptAnalyzerSettings.psd1 (walk bounded to project dir); pwsh/module absent -> clean no-op; kill switch HOOK_POWERSHELL_FORMAT_ENABLED. Fixes an inherited case-insensitive write-back bug (-ne -> -cne so casing fixes persist). Settings-file trust model documented. 39 tests; validate clean. Clean-tier wave Phase 1.
Note
Medium Risk
The hook rewrites PowerShell files on edit and PSScriptAnalyzer may load and run custom rule modules referenced from the opt-in settings file; risk is mitigated by explicit opt-in, project-dir bounds, and advisory-only behavior.
Overview
Adds a new
powershell-formatmarketplace plugin that runs on Write/Edit of.ps1,.psm1, and.psd1files. It formats in place with PSScriptAnalyzer’sInvoke-Formatterand reports lint viaInvoke-ScriptAnalyzer, using the consumer’s closestPSScriptAnalyzerSettings.psd1(walk-up bounded byCLAUDE_PROJECT_DIRwhen set). Repos without that settings file are not touched—no built-in default rules.Behavior matches sibling formatter hooks: advisory only (always exit 0, findings in
additionalContext),HOOK_POWERSHELL_FORMAT_ENABLEDkill switch, silent skip whenpwshor PSScriptAnalyzer is missing, optional telemetry, and a bundledhook-utils.shcopy. The PowerShell block fixes casing-only reformats by using-cnefor write-back, preserves encoding/BOM, skips non–UTF-8 legacy files, anchorsCustomRulePathat the settings directory, and documents the trust model for settings and custom rules.marketplace.jsonandREADME.mdcatalog entries register the plugin. A large contract test suite covers opt-in, extensions, graceful degrade, ceiling, BOM/ANSI, CustomRulePath, and telemetry.Reviewed by Cursor Bugbot for commit 3f57de3. Bugbot is set up for automated code reviews on this repo. Configure here.