A Claude Code plugin that formats and lints PowerShell the moment you edit it. On
every Write or Edit of a .ps1, .psm1, or .psd1 file it runs
PSScriptAnalyzer's
Invoke-Formatter (formatting in place) and Invoke-ScriptAnalyzer (linting),
then surfaces any residual findings back to Claude as advisory context.
It uses your repository's own analyzer settings. It ships no rules of its own
and runs only when your repo has opted into a PSScriptAnalyzerSettings.psd1.
- Spawned only for PowerShell files. The hook is registered with the
iffiltersEdit(*.ps1),Edit(*.psm1)andEdit(*.psd1), so a Write/Edit of any other file never starts a hook process for it; the extension check inside the script is unchanged. - Opt-in on a settings file. PSScriptAnalyzer runs only when a
PSScriptAnalyzerSettings.psd1governs the edited file, found by walking up from the file to the repository root and stopping at the closest one. Unlike some formatters, PSScriptAnalyzer does not auto-discover its settings.Invoke-FormatterandInvoke-ScriptAnalyzertake an explicit settings path, so the hook both gates on that file and passes it through. A repo without a settings file is left untouched rather than formatted and linted with PSScriptAnalyzer's built-in defaults, so the plugin never imposes a style you did not choose. A settings file that declaresCustomRulePathis additionally gated on explicit approval. See Trust model. - Format on edit.
Invoke-Formatterapplies your settings' formatting rules (indentation, alias expansion, brace placement, and so on) in place. - Findings are advisory. Semantic diagnostics your settings enable (for
example
PSAvoidGlobalVars) are reported but never auto-applied. - Advisory, never blocking. The hook always exits
0. Findings are reported viaadditionalContext; they never reject the edit. Make a commit hook or CI your hard gate. - Graceful degrade. If PowerShell (
pwsh) is not installed, or the PSScriptAnalyzer module is not available, the hook is a clean silent no-op. No error spam.pwshis resolved fromPATHand is never downloaded.
The opt-in PSScriptAnalyzerSettings.psd1 is executed-adjacent configuration,
not inert data. A settings file may declare a
CustomRulePath
pointing at PowerShell rule modules, and PSScriptAnalyzer loads and runs those
modules' exported functions during analysis. The hook therefore never runs the
analyzer under such a settings file without an explicit approval: it skips the
format/lint run and reports a visible trust-gate notice (once per session and agent, renewed every eighth skip, on
both the agent and user channels) naming the settings file and the approval
marker to create. To approve, review the settings file and every rule module it
references. Treat them with the same trust you give your build and CI
configuration, then create the marker directory using the exact mkdir -p
command the notice carries. The marker lives under
${CLAUDE_PLUGIN_DATA}/trust-approvals and is content-addressed over the
repository, the settings file, every file reachable under each declared
CustomRulePath entry (recursively for directories), and every repository
file those files reference by string literal (transitively, bounded), so a
change to the settings, to any referenced rule module, or to a file a rule
module loads, including a branch switch that swaps module bytes under an
unchanged settings file, revokes the approval and re-gates the run.
Detection uses PowerShell's restricted data-file parser, not a textual
scan. A settings file that parser cannot read is treated as code-loading
and stays gated. A CustomRulePath entry that does not resolve to
hashable content leaves the state unverifiable with no approval route.
When CLAUDE_PLUGIN_DATA is unavailable the gate fails closed and the
run stays skipped. A settings file without
CustomRulePath is declarative rule configuration and runs immediately. The
hook only reads a settings file at or below your project root (bounded by
CLAUDE_PROJECT_DIR when set), so it never picks up one from an ancestor
directory outside the project.
- Bash. The hook is a Bash script. On native Windows, install Git for Windows so Claude Code can run it under Git Bash.
- Node.js on
PATH. Every handler inhooks/hooks.jsonruns throughnode hooks/exec-bash.mjs, which starts the Bash script. Claude Code's native binary neither ships nor uses Node (setup docs, checked 2026-09-29), so withoutnodethe hooks do not launch and nothing is formatted, with no notice. Install Node.js. - jq on
PATH. Parses the hook payload. Absent: the hook skips with a visible notice, once per session and agent, renewed every eighth skip. Install jq. - PowerShell 7+ (
pwsh) onPATH(install). The hook probespwshonly; legacy Windows PowerShell 5.1 (powershell.exe) is not used. If absent, the edit hook stays quiet, and a SessionStart probe reports the missingpwshonce per session, fromprerequisites.json, naming/powershell-format:check. The probe runs wherever the plugin is enabled andpowershell_format_enabledis not false./powershell-format:checkis read-only and installs nothing. - The PSScriptAnalyzer module installed
(
Install-Module PSScriptAnalyzer). If absent, the hook stays quiet, with no probe notice. - A
PSScriptAnalyzerSettings.psd1in your repo. That file is the opt-in.
The hook itself runs on Bash 3.2+. Telemetry timing uses EPOCHREALTIME
(Bash 5.0+); on older bash the telemetry envelope is skipped while formatting and
linting still run.
Per docs/conventions/hook-budget/README.md,
this hook is always-on for every Write and Edit of a .ps1, .psm1 or .psd1 file (every
handler in hooks/hooks.json carries one of the three if rows, which keep every other
extension from spawning it, and the suite pins the whole handler set to the script's own
extension set), so its cost on a clean PowerShell file is the figure that counts. Measured on
Linux x86_64 under bash 5.2 in a container with HOOK_TELEMETRY_SINK and CLAUDE_PROJECT_DIR
unset, twelve interleaved trials against an interleaved bash -c : floor S of about 4 ms, with
a kernel census from strace -f -e trace=clone,clone3,fork,vfork,execve (2026-09-07, 0.7.45):
| Event | Fires | Wall | Spawn-equivalents | Kernel census |
|---|---|---|---|---|
PostToolUse Write, clean .ps1, no PSScriptAnalyzerSettings.psd1 (opt-in absent) |
1 | 39 ms | 9.3 | 15 process creations, 7 execs: three realpath, two git rev-parse (the working-tree probe and the root resolver), jq, the hook's own bash |
PostToolUse Write, clean .ps1, settings file present |
1 | 893 ms | 220 | 47 process creations, 12 execs: the row above plus one pwsh and the disclosure snapshot's mktemp, cp, cmp, rm |
PostToolUse Write, any other extension |
0 | none | 0 | no process; the if rows drop the handler before a spawn |
The opted-in row is one pwsh start-up plus PSScriptAnalyzer's module load, so at a 4 ms
floor the spawn-equivalent column measures that run time rather than spawns; the census column
is the number that transfers to the Windows 11 Git Bash reference host the convention calls
binding, and that host's figure for this plugin has not been taken. The residual is pwsh and
PSScriptAnalyzer themselves plus the shared library's payload reader (jq), working-tree probe
and root resolver (git twice, realpath) and the disclosure snapshot.
The figures above were taken on 0.7.45 (2026-09-07), before the node launcher. Each fire now
adds one node process ahead of the hook's bash, and the census has not been re-run for that
path.
/plugin marketplace add melodic-software/claude-code-plugins
/plugin install powershell-format@<marketplace>Then verify prerequisites with /powershell-format:setup check.
The formatting and linting rules come from the
PSScriptAnalyzerSettings.psd1 already in your repository, which the hook reads
automatically. To change the rules, edit that file.
One behavior knob is exposed as a native userConfig option:
| Option | Default | Effect |
|---|---|---|
powershell_format_enabled |
true |
Toggle for the powershell-format hook; set false for a clean no-op. |
Set it interactively with /plugin configure powershell-format@<marketplace>, or headless
on the install command:
claude plugin install powershell-format@<marketplace> --config powershell_format_enabled=falseGenerated from this plugin's .claude-plugin/plugin.json. Every option Claude Code
will prompt for when the plugin is enabled, with the environment variable each hook
reads it from.
| Option | Type | Default | Environment variable | Description |
|---|---|---|---|---|
powershell_format_enabled |
boolean | true |
CLAUDE_PLUGIN_OPTION_POWERSHELL_FORMAT_ENABLED |
Format and lint PowerShell on edit via PSScriptAnalyzer |
powershell_format_lint_gitignored |
boolean | false |
CLAUDE_PLUGIN_OPTION_POWERSHELL_FORMAT_LINT_GITIGNORED |
By default the hook leaves a file the repository gitignores alone: it is not rewritten or analyzed, since a rewrite of an ignored file has no git checkout to undo it. Set true to act on gitignored files too. A tracked file that matches an ignore pattern is always in scope. |
Three supported routes, in the order most people want them:
-
Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later:
/plugin configure powershell-format@<marketplace>. -
Headless. Repeat
--configfor each option. Replace<marketplace>with the marketplace you installed this plugin from:claude plugin install powershell-format@<marketplace> -s <scope> --config powershell_format_enabled=<value>
The same command reconfigures a plugin that is already installed: it prints
already installedand still writes the value. The short-circuit message is about the install, not the config write. Do notclaude plugin uninstallto reconfigure: uninstalling drops this plugin's whole storedpluginConfigsentry, resetting every option in the table above to its default.-sdefaults touser, so pass the scopeclaude plugin listreports for this plugin. The verified-version record lives in the plugin-reconfiguration convention.The value is stored immediately; the session you are in does not change. Hooks are handed their
CLAUDE_PLUGIN_OPTION_*when the session starts, so start a fresh Claude Code session before expecting new behavior. A check run in the old session still reports the old value, and that is not a failed write. -
By hand, in settings. Add the value under
pluginConfigsin your user settings (~/.claude/settings.json):{ "pluginConfigs": { "powershell-format@<marketplace>": { "options": { "powershell_format_enabled": <value> } } } }Plugin option values are read from user,
--settings, and managed settings only, not from a project's.claude/settings.json. To vary behavior per repository, enable or disable the plugin in that project'senabledPluginsinstead of setting an option there.
Do not set the CLAUDE_PLUGIN_OPTION_* variables yourself. They are how Claude Code
hands a configured value to a hook process; the value comes from the routes above.
- User configuration: the
userConfigschema and theCLAUDE_PLUGIN_OPTION_<KEY>export - Plugin install options: the
--configflag's reference entry - Plugins and skills settings:
enabledPlugins,extraKnownMarketplaces,pluginConfigs - Settings files and who they affect: user vs project vs local precedence
- Manage installed plugins: enabling, disabling,
/plugin list
MIT (SPDX-License-Identifier: MIT).