Compare commits

...
9 changed files with 92 additions and 42 deletions
+4
View File
@@ -0,0 +1,4 @@
"claude-dev": patch
---
Add Windows hooks support by running hook files with PowerShell, while keeping extensionless hook naming and file-presence semantics on Windows.
+11 -6
View File
@@ -143,16 +143,20 @@ echo '{"cancel":false}'
<Steps>
<Step title="Create the hook file">
Save the script above as `~/Documents/Cline/Hooks/file-logger` (macOS/Linux) or create it through the Hooks UI.
Save the script above as `~/Documents/Cline/Hooks/file-logger` or create it through the Hooks UI.
</Step>
<Step title="Make it executable">
Run `chmod +x ~/Documents/Cline/Hooks/file-logger` in your terminal.
On macOS/Linux, run `chmod +x ~/Documents/Cline/Hooks/file-logger`.
</Step>
<Step title="Enable it">
<Step title="Enable it (macOS/Linux only)">
In Cline's Hooks tab, find "file-logger" under PreToolUse hooks and toggle it on.
</Step>
</Steps>
<Note>
On Windows, hooks are executed with PowerShell and run whenever the hook file exists.
</Note>
### Test It
Ask Cline to read any file in your project: "What's in package.json?"
@@ -444,14 +448,15 @@ cline config set hooks-enabled=true
```
<Note>
CLI hooks are only supported on macOS and Linux.
Windows hooks require PowerShell (`powershell.exe`) available on your PATH.
</Note>
## Troubleshooting
**Hook not running?**
- Check that the file is executable (`chmod +x hookname`)
- Verify the hook is enabled (toggle is on in the Hooks tab)
- On macOS/Linux, check that the file is executable (`chmod +x hookname`)
- On Windows, ensure PowerShell is available (`powershell -NoProfile -Command "$PSVersionTable.PSVersion"`)
- On macOS/Linux, verify the hook is enabled (toggle is on in the Hooks tab)
- Check that Hooks are enabled globally in Settings
**Hook output not parsed?**
+33 -8
View File
@@ -7,6 +7,32 @@ import { escapeShellPath } from "./shell-escape"
// Maximum total output size (stdout + stderr combined)
const MAX_HOOK_OUTPUT_SIZE = 1024 * 1024 // 1MB
interface HookLaunchConfig {
command: string
args: string[]
shell: boolean
detached: boolean
}
function getHookLaunchConfig(scriptPath: string): HookLaunchConfig {
if (process.platform === "win32") {
return {
command: "powershell.exe",
args: ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath],
shell: false,
detached: false,
}
}
const escapedScriptPath = escapeShellPath(scriptPath)
return {
command: escapedScriptPath,
args: [],
shell: true,
detached: true,
}
}
/**
* HookProcess manages the execution of a hook script with streaming output capabilities.
* Similar to StandaloneTerminalProcess but specialized for hook execution.
@@ -100,16 +126,15 @@ export class HookProcess extends EventEmitter {
this.abortSignal.addEventListener("abort", abortHandler, { once: true })
}
// Spawn the hook process through shell on all platforms
// This is the git-style approach: the shell interprets the shebang line
// and executes the appropriate interpreter (bash, node, python, etc.)
// On Unix: detached=true creates a process group, allowing us to kill all children
const escapedScriptPath = escapeShellPath(this.scriptPath)
this.childProcess = spawn(escapedScriptPath, [], {
// Windows executes hooks with PowerShell directly.
// Unix executes hook files through the shell for shebang support.
const launchConfig = getHookLaunchConfig(this.scriptPath)
this.childProcess = spawn(launchConfig.command, launchConfig.args, {
stdio: ["pipe", "pipe", "pipe"],
shell: true, // Use shell on all platforms for shebang interpretation
detached: process.platform !== "win32", // Create process group on Unix
shell: launchConfig.shell,
detached: launchConfig.detached,
cwd: this.cwd, // Execute from the determined workspace root
windowsHide: true,
})
let didEmitEmptyLine = false
+2 -2
View File
@@ -33,9 +33,9 @@ describe("hooks-utils", () => {
})
})
it("should return false", () => {
it("should return true", () => {
const result = getHooksEnabledSafe()
result.should.be.false()
result.should.be.true()
})
})
+4 -5
View File
@@ -916,9 +916,8 @@ export class HookFactory {
}
/**
* Finds a hook on Windows using git-style hook discovery.
* Like git, we look for a file with the hook name (no extension) and execute it
* through the shell, which handles shebangs and script interpretation.
* Finds a hook on Windows by checking for a hook file with the canonical hook name.
* Hooks are extensionless by design (`HookName`) for parity with existing Unix naming.
*
* @param hookName the name of the hook to search for
* @param hooksDir the hooks directory path to search
@@ -933,7 +932,7 @@ export class HookFactory {
return stat.isFile() ? candidate : undefined
} catch (error) {
HookFactory.handleHookDiscoveryError(error, hookName, candidate)
// Expected error (file doesn't exist), return undefined
// Expected errors (missing/non-readable hook) return no match.
return undefined
}
}
@@ -954,7 +953,7 @@ export class HookFactory {
return stat.isFile() ? candidate : undefined
} catch (error) {
HookFactory.handleHookDiscoveryError(error, hookName, candidate)
// Expected error (file doesn't exist or not executable), return undefined
// Expected errors (missing/non-executable hook) return no match.
return undefined
}
}
+5 -15
View File
@@ -1,24 +1,14 @@
/**
* Determines if hooks are safely enabled based on platform support.
*
* Hooks are not yet supported on Windows, so this function ensures they
* remain disabled on that platform regardless of user settings.
* NOTE: This function is the single choke point used by the task runtime and
* webview state to determine the effective hooks setting.
*
* Hooks are now supported on Windows. Individual hook execution still depends
* on hook discovery and platform-specific execution details.
*
* @returns true if hooks are enabled and supported on this platform, false otherwise
*/
export function getHooksEnabledSafe(): boolean {
// Hooks are not yet supported on Windows.
//
// NOTE: This function is the single choke point used by the task runtime and
// webview state to determine the *effective* hooks setting. Hard-coding here
// ensures hooks are always enabled everywhere (TaskStart/Resume/Cancel,
// PreToolUse/PostToolUse, UI grouping) without having to override multiple
// call sites.
if (process.platform === "win32") {
return false
}
// Hard-coded: always enable hooks on supported platforms (macOS/Linux),
// regardless of persisted user setting.
return true
}
+27 -2
View File
@@ -1,10 +1,14 @@
/**
* Hook script templates for all supported hook types.
* Templates are provided as executable Bash shell scripts with comprehensive examples.
* Scripts use jq for JSON parsing when available, with fallback to basic parsing.
* On Unix, templates are Bash scripts with comprehensive examples.
* On Windows, templates are PowerShell scripts executed by the Windows hook runtime.
*/
export function getHookTemplate(hookName: string): string {
if (process.platform === "win32") {
return getWindowsPowerShellTemplate(hookName)
}
const templates: Record<string, string> = {
TaskStart: getTaskStartTemplate(),
TaskResume: getTaskResumeTemplate(),
@@ -19,6 +23,27 @@ export function getHookTemplate(hookName: string): string {
return templates[hookName] || getDefaultTemplate(hookName)
}
function getWindowsPowerShellTemplate(hookName: string): string {
return `# ${hookName} Hook
# PowerShell template for Windows hook execution.
try {
$rawInput = [Console]::In.ReadToEnd()
if ($rawInput) {
$null = $rawInput | ConvertFrom-Json
}
} catch {
Write-Error "[${hookName}] Invalid JSON input: $($_.Exception.Message)"
}
@{
cancel = $false
contextModification = ""
errorMessage = ""
} | ConvertTo-Json -Compress
`
}
function getTaskStartTemplate(): string {
return `#!/bin/bash
#
@@ -680,7 +680,9 @@ const ClineRulesToggleModal: React.FC = () => {
<>
<div className="text-xs text-description mb-4">
<p>
Toggle to enable/disable (chmod +x/-x).{" "}
{isWindows
? "On Windows, hooks execute whenever the hook file exists."
: "Toggle to enable/disable (chmod +x/-x)."}{" "}
<VSCodeLink
className="text-xs"
href="https://docs.cline.bot/features/hooks"
@@ -695,8 +697,8 @@ const ClineRulesToggleModal: React.FC = () => {
<div className="flex items-center gap-2 px-5 py-3 mb-4 bg-vscode-inputValidation-warningBackground border-l-[3px] border-vscode-inputValidation-warningBorder">
<i className="codicon codicon-warning text-sm" />
<span className="text-base">
Hook toggling is not supported on Windows. Hooks can be created, edited, and deleted,
but cannot be enabled/disabled and will not execute.
Hook toggling is not supported on Windows. Hooks can be created, edited, and deleted, and
execute whenever the hook file exists.
</span>
</div>
)}
@@ -60,7 +60,7 @@ const HookRow: React.FC<HookRowProps> = ({
<div
title={
isWindows
? "Hook toggling not supported on Windows. Hooks can be edited and deleted, but won't execute."
? "Hook toggling is not supported on Windows. Hooks execute when the hook file exists."
: undefined
}>
<Switch