Skip to content
Development
Hook

Hooks

What optim-plans runs automatically, and when. A hook is a command Claude Code fires at a fixed moment, without you asking for it.

From plugin
optim-plans
35 skills2 hooks

Claude Code

> /plugin marketplace add Optim-Agent/optim-plans
> /plugin install optim-plans@optim-plans-dev

What fires, and when

SessionStart

Fires once when a session begins, and again after a context compaction. It is where a plugin sets up its environment, or restores state the compaction dropped.

  • Matches*python3 ${CLAUDE_PLUGIN_ROOT}/hooks/optim_plans_hook.py

PreToolUse

  • Matches*python3 ${CLAUDE_PLUGIN_ROOT}/hooks/optim_plans_hook.py
Read hooks/hooks.json

Where it lives

  • hooks/optim_plans_hook.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Small optim-plans hook guard for scoped read-only refinement sessions."""
    
    from __future__ import annotations
    
    import json
    import os
    import shlex
    import sys
    from pathlib import PurePosixPath
    from typing import Any
    
    
    RESERVED_GIT = {"commit", "reset", "clean", "worktree", "update-ref"}
    
    
    def owned() -> bool:
        return bool(
            os.environ.get("OPTIM_PLANS_RUN_ID")
            or os.environ.get("OPTIM_PLANS_WORKER_NONCE")
            or os.environ.get("OPTIM_PLANS_STATE_PATH")
        )
    
    
    def validate_owned_state() -> str | None:
        if not owned():
            return None
        state_path = os.environ.get("OPTIM_PLANS_STATE_PATH")
        if not state_path:
            return "owned optim-plans session is missing OPTIM_PLANS_STATE_PATH"
        try:
            with open(state_path, encoding="utf-8") as handle:
                state = json.load(handle)
        except (OSError, json.JSONDecodeError):
            return "owned optim-plans session has invalid state"
        if state.get("run_id") != os.environ.get("OPTIM_PLANS_RUN_ID"):
            return "run ID does not match optim-plans state"
        if state.get("worker_nonce") != os.environ.get("OPTIM_PLANS_WORKER_NONCE"):
            return "session nonce does not match optim-plans state"
        return None
    
    
    def output(payload: dict[str, Any]) -> None:
        print(json.dumps(payload, ensure_ascii=True, sort_keys=True))
    
    
    def codex() -> bool:
        return bool(os.environ.get("PLUGIN_DATA") or os.environ.get("OPTIM_PLANS_PLUGIN_ROOT"))
    
    
    def pre_tool_output(decision: str, reason: str | None = None) -> dict[str, Any]:
        if decision == "allow":
            return {}
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": decision,
                "permissionDecisionReason": reason or "",
            }
        }
    
    
    def codex_session_context(context: str) -> dict[str, Any]:
        return {"hookSpecificOutput": {"hookEventName": "SessionStart", "additionalContext": context}}
    
    
    def normalize_hook_event(raw: dict[str, Any]) -> dict[str, Any]:
        event = dict(raw)
        if not isinstance(event.get("event"), str) and isinstance(raw.get("hook_event_name"), str):
            event["event"] = raw["hook_event_name"]
        if not isinstance(event.get("tool"), str) and isinstance(raw.get("tool_name"), str):
            event["tool"] = raw["tool_name"]
        tool_input = raw.get("tool_input")
        if isinstance(tool_input, dict):
            for key in ("command", "file_path", "path"):
                if key not in event and isinstance(tool_input.get(key), str):
                    event[key] = tool_input[key]
        return event
    
    
    def in_scope(path: str) -> bool:
        scopes = [item for item in os.environ.get("OPTIM_PLANS_SCOPES", "").split(os.pathsep) if item]
        if not scopes:
            return False
        target = PurePosixPath(path.replace("\\", "/"))
        if target.is_absolute() or ".." in target.parts:
            return False
        for scope in scopes:
            scope_path = PurePosixPath(scope.replace("\\", "/"))
            if target == scope_path or scope_path in target.parents:
                return True
        return False
    
    
    def handle_session_start() -> dict[str, Any]:
        ids = os.environ.get("OPTIM_PLANS_IDS", "")
        scopes = os.environ.get("OPTIM_PLANS_SCOPES", "")
        context = f"optim-plans scope: ids={ids or 'none'} scopes={scopes or 'repository'}"
        if codex():
            return codex_session_context(context)
        return {"action": "inject", "context": context}
    
    
    def is_reserved_git(command: str) -> bool:
        try:
            parts = shlex.split(command)
        except ValueError:
            return False
        if len(parts) < 2 or parts[0] != "git":
            return False
        index = 1
        options_with_values = {"-C", "-c", "--git-dir", "--work-tree"}
        while index < len(parts) and parts[index].startswith("-"):
            index += 2 if parts[index] in options_with_values else 1
        return index < len(parts) and parts[index] in RESERVED_GIT
    
    
    def handle_pre_tool(event: dict[str, Any]) -> dict[str, Any]:
        if not owned():
            return pre_tool_output("allow")
        invalid = validate_owned_state()
        if invalid:
            return pre_tool_output("deny", invalid)
        tool = event.get("tool")
        command = event.get("command")
        if tool in {"Shell", "Bash"} and isinstance(command, str) and is_reserved_git(command):
            return pre_tool_output("deny", "optim-plans reserves destructive Git state changes")
        if tool in {"Write", "Edit", "MultiEdit"}:
            path = event.get("path") or event.get("file_path")
            if not isinstance(path, str):
                return pre_tool_output("deny", "write path is missing")
            if not in_scope(path):
                return pre_tool_output("deny", f"write path {path!r} is outside OPTIM_PLANS_SCOPES")
        return pre_tool_output("allow")
    
    
    def handle(raw: dict[str, Any]) -> dict[str, Any]:
        event = normalize_hook_event(raw)
        event_name = event.get("event")
        if event_name == "SessionStart":
            if not owned():
                return {"action": "noop"}
            invalid = validate_owned_state()
            if invalid:
                return {"action": "deny", "reason": invalid}
            return handle_session_start()
        if event_name == "PreToolUse":
            return handle_pre_tool(event)
        return {"action": "noop"}
    
    
    def main() -> int:
        try:
            raw = json.load(sys.stdin)
        except json.JSONDecodeError:
            output({"action": "deny", "reason": "invalid hook JSON"})
            return 0
        output(handle(raw if isinstance(raw, dict) else {}))
        return 0
    
    
    if __name__ == "__main__":
        raise SystemExit(main())
    

Read the script before you install anything that runs on your machine. This is the one part of a plugin that acts without being asked.

@optim-agent
Ships withoptim-plans

Human-in-the-loop planning plugin for Claude and Codex: turn ideas into reviewed Markdown plans, record decisions, enforce explicit execution gates, and provide tested controller primitives for safer agent workflows.

Stars on GitHub

Momentum

stars in total
3
Star history of Optim-Agent/optim-plans on GitHub, a chart by star-history.com

More plugins that ship hooks.

superpowers

superpowers

A development methodology for coding agents that makes the agent brainstorm before building, follow plans, run parallel work and finish branches in a disciplined way. You get fewer skipped steps and more consistent results from your coding assistant.

15 skills1 hook311k
@obra@obra
Install Plugin
ponytail

ponytail

A Claude Code plugin with skills for auditing a whole repo, reviewing a change, listing deferred shortcut comments, and showing measured savings and a quick command reference.

6 skills10 hooks188k
@dietrichgebert@dietrichgebert
Install Plugin
agent-skills

agent-skills

Walks a software change from a written spec through design, testing, code review, CI/CD, and release, applying the matching engineering skill at each stage. Useful for developers using Claude Code on full development workflows.

25 skills+3112k
@addyosmani@addyosmani
Install Plugin
planning-with-files

planning-with-files

Persistent file-based planning for AI coding agents on long-running, multi-step tasks. It keeps task_plan.md, findings.md and progress.md on disk, so work can resume after /clear or context compaction.

6 skills+228k
@othmanadi@othmanadi
Install Plugin
gsd-core

gsd-core

A spec-driven development workflow for Claude Code that guides a project through planning, phases, milestone audits and fixes. Includes skills to generate tests from UAT criteria and to audit completed work.

72 skills+311k
@open-gsd@open-gsd
Install Plugin
superpowers-zh

superpowers-zh

Chinese-language skill pack for Claude Code that adds workflow guidance for brainstorming, TDD, debugging, and code review, plus Chinese conventions for commits, documentation, and domestic Git platforms like Gitee and Coding.net.

21 skills1 hook8.5k
@jnmetacode@jnmetacode
Install Plugin
socraticode

socraticode

Gives Claude Code semantic search, dependency graph and architecture context over a codebase, so an AI assistant can explore and understand a project's code. Includes skills for indexing a project and managing its file watching.

2 skills+33.4k
@giancarloerra@giancarloerra
Install Plugin
cocoindex-code

cocoindex-code

A lightweight code search engine that uses AST-based semantic search with local embeddings and incremental indexing. It helps Claude Code users find code faster and more efficiently.

1 skill1 MCP+13k
@cocoindex-io@cocoindex-io
Install Plugin

Questions about the hooks in optim-plans.

Which coding agents can use the hooks in optim-plans?
The hooks in optim-plans install in Claude Code and Cursor: each runs Claude Code hooks from .claude/settings.json, so copy the hooks there.