Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 68 additions & 12 deletions hooks/block-monk.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,77 @@
$agentDir = if ($env:MONK_AGENT_INSTALL_DIR) { $env:MONK_AGENT_INSTALL_DIR } else { Join-Path $HOME ".monk\bin" }
$agent = if ($env:MONK_AGENT_PATH) { $env:MONK_AGENT_PATH } else { Join-Path $agentDir "monk-agent.exe" }

# Let the binary read the hook payload straight from stdin. Reading it into a
# PowerShell string first and re-piping it corrupts non-ASCII bytes (e.g. the
# leading UTF-8 BOM that some hosts prepend) whenever the console is on an OEM
# code page - which is exactly what the cmd.exe launcher sets - so the binary
# would receive unparseable JSON and silently allow the command. The binary
# reads raw UTF-8 (BOM-tolerant) from the inherited stdin and is unaffected.
# Buffer stdin as bytes so the helper receives the original UTF-8 payload
# without a PowerShell console-code-page round trip. Keeping the bytes also
# lets the native parser make the decision if the helper fails or emits an
# invalid response.
$inputStream = [Console]::OpenStandardInput()
$inputBuffer = New-Object byte[] 4096
$payloadStream = New-Object System.IO.MemoryStream
while (($bytesRead = $inputStream.Read($inputBuffer, 0, $inputBuffer.Length)) -gt 0) {
$payloadStream.Write($inputBuffer, 0, $bytesRead)
}
$hookBytes = $payloadStream.ToArray()
$payloadStream.Dispose()

if (Test-Path $agent) {
& $agent hook block-monk --format claude
exit $LASTEXITCODE
# Treat the helper as authoritative only when it succeeds and emits a
# decision. An interrupted update, incompatible binary, or startup failure
# must not turn the guard off: discard the helper error and use the native
# parser below. A successful empty response also falls through safely; the
# fallback permits ordinary commands and still blocks direct `monk` calls.
$agentProcess = $null
$agentText = $null
$agentExitCode = $null
try {
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $agent
$startInfo.Arguments = "hook block-monk --format claude"
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardInput = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$startInfo.CreateNoWindow = $true

$agentProcess = New-Object System.Diagnostics.Process
$agentProcess.StartInfo = $startInfo
[void]$agentProcess.Start()
$outputTask = $agentProcess.StandardOutput.ReadToEndAsync()
$errorTask = $agentProcess.StandardError.ReadToEndAsync()
$agentProcess.StandardInput.BaseStream.Write($hookBytes, 0, $hookBytes.Length)
$agentProcess.StandardInput.BaseStream.Close()
$agentProcess.WaitForExit()
$agentText = $outputTask.GetAwaiter().GetResult()
[void]$errorTask.GetAwaiter().GetResult()
$agentExitCode = $agentProcess.ExitCode
} catch {
$agentText = $null
} finally {
if ($agentProcess) {
$agentProcess.Dispose()
}
}

if ($agentExitCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($agentText)) {
try {
$agentDecision = $agentText | ConvertFrom-Json
} catch {
$agentDecision = $null
}
if ($agentDecision.hookSpecificOutput.hookEventName -eq "PreToolUse" -and
$agentDecision.hookSpecificOutput.permissionDecision -in @("allow", "ask", "deny")) {
Write-Output $agentText
exit 0
}
}
}

# Fallback: binary unavailable. Read stdin as UTF-8 (BOM stripped) so the payload
# survives a non-UTF-8 console code page, then match `monk` in command position.
$reader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$hookInput = $reader.ReadToEnd()
# Fallback: decode the buffered UTF-8 payload and strip a leading BOM before
# matching `monk` in command position.
$hookInput = [System.Text.Encoding]::UTF8.GetString($hookBytes)
if ($hookInput.Length -gt 0 -and $hookInput[0] -eq [char]0xFEFF) {
$hookInput = $hookInput.Substring(1)
}
try { $command = ($hookInput | ConvertFrom-Json).tool_input.command } catch { exit 0 }
if (-not $command) { exit 0 }

Expand Down
80 changes: 68 additions & 12 deletions plugins/monk/hooks/block-monk.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,77 @@
$agentDir = if ($env:MONK_AGENT_INSTALL_DIR) { $env:MONK_AGENT_INSTALL_DIR } else { Join-Path $HOME ".monk\bin" }
$agent = if ($env:MONK_AGENT_PATH) { $env:MONK_AGENT_PATH } else { Join-Path $agentDir "monk-agent.exe" }

# Let the binary read the hook payload straight from stdin. Reading it into a
# PowerShell string first and re-piping it corrupts non-ASCII bytes (e.g. the
# leading UTF-8 BOM that some hosts prepend) whenever the console is on an OEM
# code page - which is exactly what the cmd.exe launcher sets - so the binary
# would receive unparseable JSON and silently allow the command. The binary
# reads raw UTF-8 (BOM-tolerant) from the inherited stdin and is unaffected.
# Buffer stdin as bytes so the helper receives the original UTF-8 payload
# without a PowerShell console-code-page round trip. Keeping the bytes also
# lets the native parser make the decision if the helper fails or emits an
# invalid response.
$inputStream = [Console]::OpenStandardInput()
$inputBuffer = New-Object byte[] 4096
$payloadStream = New-Object System.IO.MemoryStream
while (($bytesRead = $inputStream.Read($inputBuffer, 0, $inputBuffer.Length)) -gt 0) {
$payloadStream.Write($inputBuffer, 0, $bytesRead)
}
$hookBytes = $payloadStream.ToArray()
$payloadStream.Dispose()

if (Test-Path $agent) {
& $agent hook block-monk --format claude
exit $LASTEXITCODE
# Treat the helper as authoritative only when it succeeds and emits a
# decision. An interrupted update, incompatible binary, or startup failure
# must not turn the guard off: discard the helper error and use the native
# parser below. A successful empty response also falls through safely; the
# fallback permits ordinary commands and still blocks direct `monk` calls.
$agentProcess = $null
$agentText = $null
$agentExitCode = $null
try {
$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $agent
$startInfo.Arguments = "hook block-monk --format claude"
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardInput = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$startInfo.CreateNoWindow = $true

$agentProcess = New-Object System.Diagnostics.Process
$agentProcess.StartInfo = $startInfo
[void]$agentProcess.Start()
$outputTask = $agentProcess.StandardOutput.ReadToEndAsync()
$errorTask = $agentProcess.StandardError.ReadToEndAsync()
$agentProcess.StandardInput.BaseStream.Write($hookBytes, 0, $hookBytes.Length)
$agentProcess.StandardInput.BaseStream.Close()
$agentProcess.WaitForExit()
$agentText = $outputTask.GetAwaiter().GetResult()
[void]$errorTask.GetAwaiter().GetResult()
$agentExitCode = $agentProcess.ExitCode
} catch {
$agentText = $null
} finally {
if ($agentProcess) {
$agentProcess.Dispose()
}
}

if ($agentExitCode -eq 0 -and -not [string]::IsNullOrWhiteSpace($agentText)) {
try {
$agentDecision = $agentText | ConvertFrom-Json
} catch {
$agentDecision = $null
}
if ($agentDecision.hookSpecificOutput.hookEventName -eq "PreToolUse" -and
$agentDecision.hookSpecificOutput.permissionDecision -in @("allow", "ask", "deny")) {
Write-Output $agentText
exit 0
}
}
}

# Fallback: binary unavailable. Read stdin as UTF-8 (BOM stripped) so the payload
# survives a non-UTF-8 console code page, then match `monk` in command position.
$reader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$hookInput = $reader.ReadToEnd()
# Fallback: decode the buffered UTF-8 payload and strip a leading BOM before
# matching `monk` in command position.
$hookInput = [System.Text.Encoding]::UTF8.GetString($hookBytes)
if ($hookInput.Length -gt 0 -and $hookInput[0] -eq [char]0xFEFF) {
$hookInput = $hookInput.Substring(1)
}
try { $command = ($hookInput | ConvertFrom-Json).tool_input.command } catch { exit 0 }
if (-not $command) { exit 0 }

Expand Down
14 changes: 11 additions & 3 deletions tests/block-monk-windows.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ param(
$ErrorActionPreference = "Stop"
$WindowsPowerShell = Join-Path $env:SystemRoot "System32\WindowsPowerShell\v1.0\powershell.exe"
$RootHook = Join-Path $RepoRoot "hooks\block-monk.ps1"
$PluginHook = Join-Path $RepoRoot "plugins\monk\hooks\block-monk.ps1"
$AntigravityHook = Join-Path $RepoRoot ".antigravity-plugin\hooks\block-monk.ps1"
$MissingAgent = Join-Path $env:TEMP "missing-monk-agent-$PID.exe"
$PreviousAgentPath = $env:MONK_AGENT_PATH
Expand Down Expand Up @@ -58,12 +59,19 @@ function Assert-HookCases {
}

try {
$env:MONK_AGENT_PATH = $MissingAgent

Assert-HookCases -Hook $RootHook -Format "claude"
foreach ($AgentPath in @(
$MissingAgent,
(Join-Path $env:SystemRoot "System32\net.exe"),
(Join-Path $env:SystemRoot "System32\cmd.exe")
)) {
$env:MONK_AGENT_PATH = $AgentPath
Assert-HookCases -Hook $RootHook -Format "claude"
Assert-HookCases -Hook $PluginHook -Format "claude"
}

# Antigravity runs both hook siblings when bash is available. Limit PATH so
# this test exercises the stock-Windows PowerShell fallback specifically.
$env:MONK_AGENT_PATH = $MissingAgent
$env:Path = Split-Path -Parent $WindowsPowerShell
Assert-HookCases -Hook $AntigravityHook -Format "antigravity"
} finally {
Expand Down