Skip to content

Instantly share code, notes, and snippets.

@arunkumar9t2
Created February 16, 2026 04:08
Show Gist options
  • Select an option

  • Save arunkumar9t2/ad88a356a4e06c6e73819a094e5d08ee to your computer and use it in GitHub Desktop.

Select an option

Save arunkumar9t2/ad88a356a4e06c6e73819a094e5d08ee to your computer and use it in GitHub Desktop.
Ensure claude always plans in a certain with hooks
"""Validate plan files for task management completeness in beads projects.
This validator checks if plans written to ~/.claude/plans/*.md include:
1. A step to set the task to "in progress" (always required)
2. A step to close the task with a reason (always required)
3. A step to check/notify downstream tasks (required when making architectural changes)
"""
from __future__ import annotations
import logging
import subprocess
from pathlib import Path
from typing import Any
from ..base import Handler
from ..models import HookInput
from ..outputs import block_stop
from ..utils import is_beads_project
logger = logging.getLogger(__name__)
class BeadsPlanValidator(Handler):
"""Ensure plans in beads projects have complete task lifecycle management."""
events = ["PostToolUse"]
def handle(self, hook_input: HookInput, context: dict[str, Any] | None = None):
"""Validate that plans have complete task lifecycle management.
Intercepts Write tool calls to ~/.claude/plans/*.md files and uses
Claude CLI to evaluate whether the plan includes:
- Setting task to in-progress
- Closing task with a reason
- Notifying downstream tasks (when making architectural changes)
"""
# Only process Write tool
if hook_input.tool_name != "Write":
return None
# Get file path from tool_input
tool_input = hook_input.tool_input or {}
file_path = tool_input.get("file_path", "")
# Only plans/*.md files
if "/plans/" not in file_path or not file_path.endswith(".md"):
return None
# Exclude home directory and ~/.claude from beads validation
cwd_path = Path(hook_input.cwd).resolve()
home_path = Path.home().resolve()
claude_config_path = home_path / ".claude"
if cwd_path == home_path or cwd_path == claude_config_path:
logger.debug(f"Skipping beads validation in config directory: {cwd_path}")
return None
# Only beads projects
if not is_beads_project(hook_input.cwd):
logger.debug("Not a beads project - skipping")
return None
# Get plan content directly from tool_input
content = tool_input.get("content", "")
if not content:
logger.debug("No content in Write tool_input")
return None
logger.info(f"Plan file written in beads project: {file_path} - evaluating downstream consideration")
# Call Claude CLI to evaluate the plan
return self._evaluate_plan(content, hook_input.cwd)
def _evaluate_plan(self, plan_content: str, cwd: str):
"""Use Claude CLI to evaluate the plan for task management completeness."""
prompt = f"""Evaluate this implementation plan for beads task management completeness.
PLAN:
{plan_content[:30000]}
EVALUATION CRITERIA (check ALL):
1. TASK LIFECYCLE (ALWAYS REQUIRED):
a) Does the plan include a step to set the task to "in progress"?
- Look for: "in progress", "in_progress", "bd set --status", "mark as in progress", "start working"
b) Does the plan include a step to close the task with a reason at the end?
- Look for: "close task", "bd close", "complete task with reason", "--reason", "summarizing changes"
2. DOWNSTREAM NOTIFICATION (CONDITIONAL):
- Only check this if the plan makes KEY ARCHITECTURAL DECISIONS (API changes, schema changes, interface modifications, shared component changes, breaking changes)
- If yes, does it include an EXPLICIT step to check/notify downstream tasks?
- Look for: "downstream", "dependent tasks", "bd list --blocked-by", "notify", "update blocked tasks"
RESPOND WITH:
- APPROVE - if all required criteria are met
- BLOCK: <detailed reason> - if any required criteria is missing
If blocking, your reason MUST be specific and actionable. List each missing item with what needs to be added. Examples:
- "Missing step to set task to in-progress before starting work"
- "Missing final step to close task with --reason flag summarizing changes"
- "Plan modifies shared API but lacks step to check downstream tasks with 'bd list --blocked-by <task-id>' and notify/update those tasks about the breaking changes"
"""
try:
result = subprocess.run(
["claude", "-p", prompt, "--allowedTools", ""],
capture_output=True,
text=True,
timeout=45,
cwd=cwd,
)
response = result.stdout.strip()
logger.info(f"Claude evaluation result: {response[:200]}...")
if "BLOCK" in response.upper():
# Use Claude's response directly - it already contains the descriptive reason
return block_stop(f"REPLAN: {response}")
except subprocess.TimeoutExpired:
logger.warning("Claude CLI evaluation timed out")
except FileNotFoundError:
logger.error("Claude CLI not found in PATH")
except Exception as e:
logger.error(f"Error evaluating plan: {e}")
# On error or approval, don't block
return None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment