Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

powershell-format

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.

Behavior

  • Spawned only for PowerShell files. The hook is registered with the if filters Edit(*.ps1), Edit(*.psm1) and Edit(*.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.psd1 governs 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-Formatter and Invoke-ScriptAnalyzer take 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 declares CustomRulePath is additionally gated on explicit approval. See Trust model.
  • Format on edit. Invoke-Formatter applies 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 via additionalContext; 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. pwsh is resolved from PATH and is never downloaded.

Trust model

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.

Requirements

  • 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 in hooks/hooks.json runs through node 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 without node the 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) on PATH (install). The hook probes pwsh only; legacy Windows PowerShell 5.1 (powershell.exe) is not used. If absent, the edit hook stays quiet, and a SessionStart probe reports the missing pwsh once per session, from prerequisites.json, naming /powershell-format:check. The probe runs wherever the plugin is enabled and powershell_format_enabled is not false. /powershell-format:check is read-only and installs nothing.
  • The PSScriptAnalyzer module installed (Install-Module PSScriptAnalyzer). If absent, the hook stays quiet, with no probe notice.
  • A PSScriptAnalyzerSettings.psd1 in 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.

Hook budget accounting

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.

Install

/plugin marketplace add melodic-software/claude-code-plugins
/plugin install powershell-format@<marketplace>

Then verify prerequisites with /powershell-format:setup check.

Configuration

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=false

Options reference

Generated 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.

How to set these

Three supported routes, in the order most people want them:

  1. Interactively. Claude Code prompts for declared options when you enable the plugin. To change them later: /plugin configure powershell-format@<marketplace>.

  2. Headless. Repeat --config for 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 installed and still writes the value. The short-circuit message is about the install, not the config write. Do not claude plugin uninstall to reconfigure: uninstalling drops this plugin's whole stored pluginConfigs entry, resetting every option in the table above to its default. -s defaults to user, so pass the scope claude plugin list reports 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.

  3. By hand, in settings. Add the value under pluginConfigs in 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's enabledPlugins instead 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.

Upstream documentation

License

MIT (SPDX-License-Identifier: MIT).