Many years later, a summary as of:
Windows PowerShell v5.1 - called WinPS below - the legacy, ships-with-Windows, Windows-only edition of PowerShell whose latest and final version is 5.1, whose CLI is powershell.exe
PowerShell (Core) 7 v7.5.x - called PS7 below - the modern, cross-platform, install-on-demand edition, whose CLI is pwsh (pwsh.exe on Windows)
The CLIs of both editions require that a script file passed to it on Windows have a .ps1 filename extension,[1] which Git hook scripts (e.g., pre-commmit) by design do not have.
- This requirement was originally lifted for now-obsolete PS7 versions (up to v7.1.x) on Windows too, but it was reinstated in v7.2, with a stated rationale of, "[...] Windows doesn't support shebang, so we should be consistent with Windows PowerShell and only allow for scripts with
.ps1 extension."
Direct use of PowerShell code in Git hook scripts using a shebang line such as #!/usr/bin/env pwsh is therefore NOT an option on Windows[2] as of PS7 v7.5.x, but that is unlikely to change.
- That said, if there's enough community interest, perhaps the
.ps1 requirement will be lifted again in a future PS7 version (but not for WinPS, which will only see security-critical fixes going forward); you can signal your interest by giving GitHub issue #26584 a 👍.
Workaround:
Note:
The workaround below assumes that execution of PowerShell scripts has already been allowed on a given target machine, such as with one-time configuration command
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -Force; if not, you can enable script execution ad hoc by adding -ExecutionPolicy Bypass to the CLI calls below, assuming that there aren't more restrictive GPO-based policies in place; see this answer for more information.
The following workaround merely requires placing a single, static line of Bash (sh) code after the default shebang line, #!/bin/sh.
The rest of the file can then directly contain PowerShell code.
The Bash code creates a copy of the hook script at hand in a temporary file with extension .ps1, invokes PowerShell with that temporary file, then cleans up the latter and passes the PowerShell process' exit code through.
The approach works in cross-platform manner if you target pwsh, i.e. the PS7 CLI, i.e. equally on Windows and Unix-like platforms.
However, if you only need Windows support and want to use WinPS so you don't have to install PS7, all you need to do is replace pwsh with powershell below.
Either way, the Bash line assumes that the targeted PowerShell executable is discoverable via a directory listed in the PATH environment variable; this is true by default for WinPS and typically true for PS7, depending on the installation method.
The following proof-of-concept shows the workaround with a trivial piece of PowerShell code placed directly in a hook script, e.g., ./git/hooks/pre-commit:
#!/bin/sh
tmpPs1Script="$(mktemp -u).ps1"; tail +3 "$0" > "$tmpPs1Script"; pwsh -NoProfile -File "$tmpPs1Script"; ec=$?; rm "$tmpPs1Script"; exit $ec
# The above two lines would be the same for all hook scripts that you want to
# run with PowerShell.
# Place your PowerShell code below.
"Hi from a hook script on $(Get-Date)"
Note:
For cross-platform use, on Unix-like platforms, don't forget to make your hook scripts executable first, e.g. chmod +x ./git/hooks/pre-commit
The command used to create the temporary .ps1 script,
tmpPs1Script="$(mktemp -u).ps1", has been chosen for compatibility with macOS too, but comes with a small, largely hypothetical caveat: another, concurrently running process hypothetically could try to use the same file path.
- Therefore, a (marginally) more robust option on Linux platforms / platforms with GNU utilities (which includes Git Bash on Windows) is to use
tmpPs1Script="$(mktemp --suffix=.ps1)" instead.
A concrete application of this technique can be found in the bottom section of this answer.
[1] Note that a shebang line that targets PowerShell e.g. #!/usr/bin/env powershell (WinPS) or #!/usr/bin/env pwsh (PS7) would in effect pass the invoked script's file path via the default CLI parameter, which differs by edition: In WinPS, it is -Command (-c), in PS7 it is -File (-f). While technically only the -File parameter enforces the presence of a .ps1 extension in its argument, passing an extension-less file (such as in the case of Git hook scripts) to -Command wouldn't work as expected: due to lacking an executable extension, such a file is treated as a document and opening it is delegated to the Windows shell, which pops up a What-application-do-you-want-to-open-this GUI dialog, asynchronously.
[2] Simon Elms' helpful answer demonstrates a clever workaround, which, however, requires creating a separate helper script with extension .ps1 for each hook script of interest, with the hook script itself containing only a WinPS shebang line, #!/usr/bin/env powershell; to make the approach work in PS7, you'd have to use #!/usr/bin/env -S pwsh -Command.
githooksmanpage. I have no idea if there is different documentation provided for the Windows version(s).powershell -file someScript.ps1 args