Security best practices
Keep these practices in mind when writing hooks:
- Validate and sanitize inputs: never trust input data blindly
- Always quote shell variables: use
"$VAR"not$VAR - Block path traversal: check for
..in file paths - Use absolute paths: specify full paths for scripts. In exec form, use
$\{CLAUDE_PROJECT_DIR\}and the path needs no quoting. In shell form, wrap it in double quotes - Skip sensitive files: avoid
.env,.git/, keys, etc.
Windows PowerShell tool
On Windows, you can run individual hooks in PowerShell by setting "shell": "powershell" on a command hook. Claude Code auto-detects pwsh.exe, the PowerShell 7 and later executable, and falls back to powershell.exe for Windows PowerShell 5.1.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"shell": "powershell",
"command": "Write-Host 'File written'"
}
]
}
]
}
}To reference the project root from a PowerShell shell-form command, write ${CLAUDE_PROJECT_DIR} or $env:CLAUDE_PROJECT_DIR. As of v2.1.198, Claude Code rewrites the ${CLAUDE_PROJECT_DIR}, ${CLAUDE_PLUGIN_ROOT\}, and ${CLAUDE_PLUGIN_DATA} placeholders in a PowerShell shell-form command to PowerShell's ${env:NAME\} form, whether the hook is defined in settings.json, a plugin, or a skill. PowerShell then resolves the value from the exported environment after parsing, so the placeholder works inside double-quoted strings but not inside single-quoted strings, where PowerShell never expands variables.
Before v2.1.198, this rewrite applied only to plugin hooks. On earlier versions, a settings.json hook needs the $env: form or exec form, where ${CLAUDE_PROJECT_DIR\} is substituted in each args element regardless of where the hook is defined.
Don't write the bare $CLAUDE_PROJECT_DIR spelling in a PowerShell hook. PowerShell parses it as an undefined local variable and resolves it to $null, which leaves the script path without its project-root prefix. Claude Code doesn't rewrite that form; it logs a warning in the debug log instead.
The example below shows a settings.json hook that runs a project script with the $env: form, which works on every version:
{
"type": "command",
"shell": "powershell",
"command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\check.ps1\""
}