Skip to content
Open
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
125 changes: 106 additions & 19 deletions .antigravity-plugin/hooks/block-monk.ps1
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# PreToolUse hook for the run_command tool: block any shell-out to the `monk` CLI.
# PreToolUse hook for the run_command tool: block any shell-out to the `monk`
# CLI or `monkd` daemon.
# Windows (stock, no Git Bash) counterpart of block-monk.sh.
#
# Antigravity PreToolUse I/O:
# stdin: {"toolCall":{"name":"run_command","args":{"CommandLine":"..."}},...}
# stdout: {"decision":"deny","reason":"..."} to block, or exit 0 to allow
#
# Delegates to `monk-agent hook block-monk --format antigravity`; falls back to a
# native regex biased toward BLOCKING when the binary is unavailable. Always
# exits 0 (the deny JSON is the block signal).
# Delegates to `monk-agent hook block-monk --format antigravity`; post-filters
# helper allow/no-output results and falls back to a native regex biased toward
# BLOCKING when the binary is unavailable. Always exits 0 (the deny JSON is the
# block signal).

$ErrorActionPreference = "SilentlyContinue"

Expand All @@ -19,27 +21,112 @@ if ($env:OS -ne 'Windows_NT' -and (Get-Command bash -ErrorAction SilentlyContinu
$InstallDir = 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 $InstallDir "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. a
# leading UTF-8 BOM) under an OEM console code page - which the cmd.exe launcher
# sets - so the binary would get unparseable JSON. The binary reads raw UTF-8
# (BOM-tolerant) from the inherited stdin and is unaffected.
if (Test-Path $agent) {
& $agent hook block-monk --format antigravity
exit $LASTEXITCODE
function Read-StandardInputBytes {
$stream = [Console]::OpenStandardInput()
$buffer = New-Object byte[] 8192
$memory = New-Object System.IO.MemoryStream
try {
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$memory.Write($buffer, 0, $read)
}
$memory.ToArray()
} finally {
$memory.Dispose()
}
}

# Fallback: binary unavailable. Read stdin as UTF-8 (BOM stripped), match `monk`.
$reader = New-Object System.IO.StreamReader([Console]::OpenStandardInput(), [System.Text.Encoding]::UTF8)
$hookInput = $reader.ReadToEnd()
try { $command = ($hookInput | ConvertFrom-Json).toolCall.args.CommandLine } catch { exit 0 }
if (-not $command) { exit 0 }
function Invoke-AgentHook {
param([byte[]]$InputBytes)

$startInfo = New-Object System.Diagnostics.ProcessStartInfo
$startInfo.FileName = $agent
$startInfo.Arguments = "hook block-monk --format antigravity"
$startInfo.UseShellExecute = $false
$startInfo.RedirectStandardInput = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
$startInfo.CreateNoWindow = $true

$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
[void]$process.Start()
if ($InputBytes.Length -gt 0) {
$process.StandardInput.BaseStream.Write($InputBytes, 0, $InputBytes.Length)
}
$process.StandardInput.Close()
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()

if ($command -match '(^|[\r\n;&|`({])\s*(sudo\s+)?monk(\s|$)') {
@{
ExitCode = $process.ExitCode
Stdout = $stdout
Stderr = $stderr
}
}

function Test-MonkShellCommand {
param([string]$Command)

$binary = '(?:(?:[A-Za-z]:\\|~[\\/]|/)(?:[^\s"'';&|(){}]+[\\/])*)?monkd?(?:\.exe)?'
$boundary = '(^|[\r\n;&|`({])\s*'
$prefix = '(?:(?:sudo|command)\s+)*(?:env(?:\s+(?:-\S+|[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|''[^'']*''|[^\s]+)))*\s+)?'
$tail = '(\s|$|[;&|`"''\)])'
$quote = '(?:["'']\s*)?'
$inlineLead = '(?:[^"'';&|]*[;&|]\s*)?'

if ($Command -match ($boundary + $prefix + $binary + $tail)) { return $true }
if ($Command -match ($boundary + '\$\([^\r\n)]*\bwhich\s+monkd?\b[^\r\n)]*\)\s+\S+')) { return $true }

$shell = '(?:bash|sh|zsh)(?:\.exe)?'
if ($Command -match ($boundary + '(?:sudo\s+)?' + $shell + '\s+-[A-Za-z]*c\s+' + $quote + $inlineLead + $prefix + $binary + $tail)) {
return $true
}

$powerShell = '(?:powershell|powershell\.exe|pwsh|pwsh\.exe)'
if ($Command -match ($boundary + '(?:sudo\s+)?' + $powerShell + '\b[^\r\n;&|]*\s-(?:Command|c)\s+' + $quote + $inlineLead + $prefix + $binary + $tail)) {
return $true
}

return $false
}

function Write-Deny {
@{
decision = "deny"
reason = "Blocked: do not shell out to the ``monk`` CLI - it desyncs the cluster state Monk manages. Use the monk-agent MCP tools instead."
reason = "Blocked: do not shell out to the ``monk`` CLI or ``monkd`` daemon - it desyncs the cluster state Monk manages. Use the monk-agent MCP tools instead."
} | ConvertTo-Json -Compress
}

$hookBytes = Read-StandardInputBytes
$agentResult = $null

if (Test-Path $agent) {
$agentResult = Invoke-AgentHook -InputBytes $hookBytes
if ($agentResult["Stdout"] -match '"decision"\s*:\s*"deny"') {
[Console]::Out.Write($agentResult["Stdout"])
exit 0
}
if ($agentResult["ExitCode"] -ne 0) {
if ($agentResult["Stdout"]) { [Console]::Out.Write($agentResult["Stdout"]) }
if ($agentResult["Stderr"]) { [Console]::Error.Write($agentResult["Stderr"]) }
exit $agentResult["ExitCode"]
}
}

$hookInput = [System.Text.Encoding]::UTF8.GetString($hookBytes)
if ($hookInput.Length -gt 0 -and [int][char]$hookInput[0] -eq 0xFEFF) {
$hookInput = $hookInput.Substring(1)
}

try { $command = ($hookInput | ConvertFrom-Json).toolCall.args.CommandLine } catch { $command = "" }
if ($command -and (Test-MonkShellCommand -Command $command)) {
Write-Deny
exit 0
}

if ($agentResult -and $agentResult["Stdout"]) {
[Console]::Out.Write($agentResult["Stdout"])
}

exit 0
57 changes: 46 additions & 11 deletions .antigravity-plugin/hooks/block-monk.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env sh
# PreToolUse hook for the run_command tool: block any shell-out to the `monk` CLI.
# Monk owns its own cluster state — running `monk ...` from a shell desyncs it.
# PreToolUse hook for the run_command tool: block any shell-out to the `monk`
# CLI or `monkd` daemon. Monk owns its own cluster state — running these from a shell desyncs it.
# Use monk-agent MCP tools instead.
#
# Antigravity PreToolUse I/O:
Expand All @@ -23,23 +23,58 @@ if [ -t 0 ]; then exit 0; fi

input="$(cat)"

agent="${MONK_AGENT_PATH:-${MONK_AGENT_INSTALL_DIR:-"$HOME/.monk/bin"}/monk-agent}"
if [ -x "$agent" ]; then
if printf '%s' "$input" | "$agent" hook block-monk --format antigravity; then
exit 0
should_block_monk_shellout() {
payload=$1
boundary='(^|[";&|`({]|\\n|\\r\\n)[[:space:]]*'
monk_bin='((([A-Za-z]:\\\\|~[\\/]|/)([^[:space:]";&|(){}]+[\\/])*)?monkd?(\.exe)?)'
prefix='((sudo|command)[[:space:]]+)*(env([[:space:]]+(-[^[:space:]]+|[A-Za-z_][A-Za-z0-9_]*=("[^"]*"|[^[:space:]]+)))*[[:space:]]+)?'

if printf '%s' "$payload" | grep -Eq "${boundary}${prefix}${monk_bin}([[:space:]\";&|)]|$)"; then
return 0
fi

if printf '%s' "$payload" | grep -Eq "${boundary}\\\$\\([^)]*which[[:space:]]+monkd?[^)]*\\)[[:space:]]+[^[:space:]]"; then
return 0
fi
fi

# Fallback: binary unavailable. Grep the raw hook payload for a `monk` command
# in command position. False positives only ever BLOCK, never allow.
if printf '%s' "$input" | grep -Eq '(^|["[:space:];&|`(])(sudo[[:space:]]+)?monk([[:space:]"]|$)'; then
if printf '%s' "$payload" | grep -Eq "${boundary}(sudo[[:space:]]+)?(bash|sh|zsh)(\\.exe)?[[:space:]]+-[A-Za-z]*c[[:space:]]+(\\\\?\")?[[:space:]]*([^\";&|]*[;&|][[:space:]]*)?${prefix}${monk_bin}([[:space:]\";&|)]|$)"; then
return 0
fi

if printf '%s' "$payload" | grep -Eq "${boundary}(sudo[[:space:]]+)?(powershell|powershell\\.exe|pwsh|pwsh\\.exe)[^;&|]*[[:space:]]-(Command|c)[[:space:]]+(\\\\?\")?[[:space:]]*([^\";&|]*[;&|][[:space:]]*)?${prefix}${monk_bin}([[:space:]\";&|)]|$)"; then
return 0
fi

return 1
}

emit_deny() {
cat <<'JSON'
{
"decision": "deny",
"reason": "Blocked: do not shell out to the `monk` CLI — it desyncs the cluster state Monk manages. Use the monk-agent MCP tools instead."
"reason": "Blocked: do not shell out to the `monk` CLI or `monkd` daemon — it desyncs the cluster state Monk manages. Use the monk-agent MCP tools instead."
}
JSON
}

agent="${MONK_AGENT_PATH:-${MONK_AGENT_INSTALL_DIR:-"$HOME/.monk/bin"}/monk-agent}"
agent_output=
if [ -x "$agent" ]; then
if agent_output="$(printf '%s' "$input" | "$agent" hook block-monk --format antigravity)"; then
if printf '%s' "$agent_output" | grep -Eq '"decision"[[:space:]]*:[[:space:]]*"deny"'; then
printf '%s' "$agent_output"
exit 0
fi
fi
fi

if should_block_monk_shellout "$input"; then
emit_deny
exit 0
fi

if [ -n "$agent_output" ]; then
printf '%s' "$agent_output"
fi

exit 0
125 changes: 105 additions & 20 deletions hooks/block-monk.ps1
Original file line number Diff line number Diff line change
@@ -1,39 +1,124 @@
# PreToolUse hook for the Bash tool: block any shell-out to the `monk` CLI.
# Monk owns its own cluster state - running `monk ...` from a shell desyncs it.
# PreToolUse hook for the Bash tool: block any shell-out to the `monk` CLI or
# `monkd` daemon.
# Monk owns its own cluster state - running these from a shell desyncs it.
# Use monk-agent MCP tools instead.
#
# Delegates to `monk-agent hook block-monk` so the logic stays in one place.
# Falls back to native PowerShell if the binary is unavailable.
# Post-filters helper allow/no-output results and falls back to native
# PowerShell if the binary is unavailable.

$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.
if (Test-Path $agent) {
& $agent hook block-monk --format claude
exit $LASTEXITCODE
function Read-StandardInputBytes {
$stream = [Console]::OpenStandardInput()
$buffer = New-Object byte[] 8192
$memory = New-Object System.IO.MemoryStream
try {
while (($read = $stream.Read($buffer, 0, $buffer.Length)) -gt 0) {
$memory.Write($buffer, 0, $read)
}
$memory.ToArray()
} finally {
$memory.Dispose()
}
}

function Invoke-AgentHook {
param([byte[]]$InputBytes)

$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

$process = New-Object System.Diagnostics.Process
$process.StartInfo = $startInfo
[void]$process.Start()
if ($InputBytes.Length -gt 0) {
$process.StandardInput.BaseStream.Write($InputBytes, 0, $InputBytes.Length)
}
$process.StandardInput.Close()
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()

@{
ExitCode = $process.ExitCode
Stdout = $stdout
Stderr = $stderr
}
}

# 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()
try { $command = ($hookInput | ConvertFrom-Json).tool_input.command } catch { exit 0 }
if (-not $command) { exit 0 }
function Test-MonkShellCommand {
param([string]$Command)

$binary = '(?:(?:[A-Za-z]:\\|~[\\/]|/)(?:[^\s"'';&|(){}]+[\\/])*)?monkd?(?:\.exe)?'
$boundary = '(^|[\r\n;&|`({])\s*'
$prefix = '(?:(?:sudo|command)\s+)*(?:env(?:\s+(?:-\S+|[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|''[^'']*''|[^\s]+)))*\s+)?'
$tail = '(\s|$|[;&|`"''\)])'
$quote = '(?:["'']\s*)?'
$inlineLead = '(?:[^"'';&|]*[;&|]\s*)?'

if ($Command -match ($boundary + $prefix + $binary + $tail)) { return $true }
if ($Command -match ($boundary + '\$\([^\r\n)]*\bwhich\s+monkd?\b[^\r\n)]*\)\s+\S+')) { return $true }

$shell = '(?:bash|sh|zsh)(?:\.exe)?'
if ($Command -match ($boundary + '(?:sudo\s+)?' + $shell + '\s+-[A-Za-z]*c\s+' + $quote + $inlineLead + $prefix + $binary + $tail)) {
return $true
}

$powerShell = '(?:powershell|powershell\.exe|pwsh|pwsh\.exe)'
if ($Command -match ($boundary + '(?:sudo\s+)?' + $powerShell + '\b[^\r\n;&|]*\s-(?:Command|c)\s+' + $quote + $inlineLead + $prefix + $binary + $tail)) {
return $true
}

if ($command -match '(^|[\r\n;&|`({])\s*(sudo\s+)?monk(\s|$)') {
return $false
}

function Write-Deny {
@{
hookSpecificOutput = @{
hookEventName = "PreToolUse"
permissionDecision = "deny"
permissionDecisionReason = "Blocked: do not shell out to the ``monk`` CLI - it desyncs the cluster state Monk manages. Use the monk-agent MCP tools instead."
permissionDecisionReason = "Blocked: do not shell out to the ``monk`` CLI or ``monkd`` daemon - it desyncs the cluster state Monk manages. Use the monk-agent MCP tools instead."
}
} | ConvertTo-Json -Compress
}

$hookBytes = Read-StandardInputBytes
$agentResult = $null

if (Test-Path $agent) {
$agentResult = Invoke-AgentHook -InputBytes $hookBytes
if ($agentResult["Stdout"] -match '"permissionDecision"\s*:\s*"deny"') {
[Console]::Out.Write($agentResult["Stdout"])
exit 0
}
if ($agentResult["ExitCode"] -ne 0) {
if ($agentResult["Stdout"]) { [Console]::Out.Write($agentResult["Stdout"]) }
if ($agentResult["Stderr"]) { [Console]::Error.Write($agentResult["Stderr"]) }
exit $agentResult["ExitCode"]
}
}

$hookInput = [System.Text.Encoding]::UTF8.GetString($hookBytes)
if ($hookInput.Length -gt 0 -and [int][char]$hookInput[0] -eq 0xFEFF) {
$hookInput = $hookInput.Substring(1)
}

try { $command = ($hookInput | ConvertFrom-Json).tool_input.command } catch { $command = "" }
if ($command -and (Test-MonkShellCommand -Command $command)) {
Write-Deny
exit 0
}

if ($agentResult -and $agentResult["Stdout"]) {
[Console]::Out.Write($agentResult["Stdout"])
}

exit 0
Loading