fix: distinguish phase file writes from reads in PostToolUse hook

- Parse tool_name via jq: Write tool checks file_path match,
  Bash tool checks for redirect operator (>) with phase file path
- Reads (cat, head) no longer trigger false-positive markers
- Split guard into separate statements for clarity
- Move marker cleanup inside hook-install guard
- Expand tests: 5 cases covering Bash write, Write tool, Bash read,
  unrelated Bash, and Write to different file

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
openhands 2026-03-19 18:14:49 +00:00
parent ac04dc29a6
commit 809dd93c3b
3 changed files with 67 additions and 18 deletions

View file

@ -110,8 +110,8 @@ create_agent_session() {
}
}' > "$settings"
fi
rm -f "$phase_marker"
fi
rm -f "$phase_marker"
fi
rm -f "$idle_marker"

View file

@ -2,9 +2,9 @@
# on-phase-change.sh — PostToolUse hook for phase file write detection.
#
# Called by Claude Code after every Bash|Write tool execution.
# Checks if the tool input references the phase file path and, if so,
# writes a "phase-changed" timestamp marker so monitor_phase_loop can
# react immediately instead of waiting for the next mtime-based poll.
# Detects writes (not reads) to the phase file and writes a timestamp
# marker so monitor_phase_loop can react immediately instead of waiting
# for the next mtime-based poll.
#
# Usage (in .claude/settings.json):
# {"type": "command", "command": "this-script /path/to/phase-file /path/to/marker"}
@ -14,11 +14,30 @@
phase_file="${1:-}"
marker_file="${2:-}"
[ -z "$phase_file" ] && exit 0
[ -z "$marker_file" ] && exit 0
input=$(cat) # consume hook JSON from stdin
[ -z "$phase_file" ] || [ -z "$marker_file" ] && exit 0
# Fast path: skip if phase file not referenced at all
printf '%s' "$input" | grep -qF "$phase_file" || exit 0
# Check if the tool input references the phase file path
if printf '%s' "$input" | grep -qF "$phase_file"; then
date +%s > "$marker_file"
fi
# Parse tool type and detect writes only (ignore reads like cat/head)
tool_name=$(printf '%s' "$input" | jq -r '.tool_name // empty' 2>/dev/null)
case "$tool_name" in
Write)
# Write tool: check if file_path targets the phase file
file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null)
[ "$file_path" = "$phase_file" ] && date +%s > "$marker_file"
;;
Bash)
# Bash tool: check if the decoded command contains a redirect (>)
# targeting the phase file — distinguishes writes from reads
command_str=$(printf '%s' "$input" | jq -r '.tool_input.command // empty' 2>/dev/null)
if printf '%s' "$command_str" | grep -qF "$phase_file" \
&& printf '%s' "$command_str" | grep -q '>'; then
date +%s > "$marker_file"
fi
;;
esac