Skip to content
Development
Hook

Hooks

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

From plugin
fable-ish
411 skill6 hooks

Claude Code

> /plugin marketplace add chrisryugj/fable-ish
> /plugin install fable-ish@fable-ish

What fires, and when

UserPromptSubmit

Fires before Claude sees each prompt you send. A plugin can use it to inject context, so the same instruction reaches the model every turn instead of only at session start.

  • python3 "${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_submit.py"

PreToolUse

  • Matches^(Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit)$python3 "${CLAUDE_PLUGIN_ROOT}/hooks/pre_tool_use.py"

PermissionRequest

  • Matches^(Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit)$python3 "${CLAUDE_PLUGIN_ROOT}/hooks/permission_request.py"

PostToolUse

  • Matches^(Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit)$python3 "${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_use.py"

PostToolUseFailure

  • Matches^(Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit)$python3 "${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_use.py"

Stop

  • python3 "${CLAUDE_PLUGIN_ROOT}/hooks/stop_gate.py"
Read hooks/hooks.json

Where it lives

  • hooks/permission_request.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Approval-time guardrails for fable-ish."""
    
    from __future__ import annotations
    
    import sys
    from pathlib import Path
    
    ROOT = Path(__file__).resolve().parents[1]
    sys.path.insert(0, str(ROOT / "scripts"))
    
    from classify_task import classify_tool_risk
    from ledger import add_unique, emit_json, read_stdin_json, update_ledger
    
    
    def main() -> int:
        input_data = read_stdin_json()
        blocked, flags, reason = classify_tool_risk(input_data)
        if flags:
            update_ledger(input_data, lambda ledger: add_unique(ledger, "risk_flags", flags))
        if blocked:
            emit_json(
                {
                    "hookSpecificOutput": {
                        "hookEventName": "PermissionRequest",
                        "decision": {
                            "behavior": "deny",
                            "message": f"fable-ish denied approval request: {reason}",
                        },
                    }
                }
            )
        else:
            emit_json({})
        return 0
    
    
    if __name__ == "__main__":
        try:
            raise SystemExit(main())
        except Exception as exc:
            emit_json({"systemMessage": f"fable-ish permission hook failed open: {exc}"})
            raise SystemExit(0)
    
  • hooks/post_tool_use.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Record fable-ish tool evidence after supported tool calls."""
    
    from __future__ import annotations
    
    import sys
    from pathlib import Path
    
    ROOT = Path(__file__).resolve().parents[1]
    sys.path.insert(0, str(ROOT / "scripts"))
    
    from ledger import add_unique, emit_json, read_stdin_json, redact, update_ledger
    from parse_tool_result import (
        changed_kinds,
        changed_paths,
        command_from_input,
        detect_failure,
        verification_coverage,
        verification_record,
    )
    
    
    def main() -> int:
        input_data = read_stdin_json()
        event = str(input_data.get("hook_event_name") or "PostToolUse")
        failure_event = event == "PostToolUseFailure"
        kinds = changed_kinds(input_data)
        paths = changed_paths(input_data)
        failure = detect_failure(input_data)
        verification = verification_record(input_data)
        command = command_from_input(input_data)
    
        # An explicit failure event means the tool did not succeed: never let it be
        # recorded as a passing verification, and record a failure even if the
        # heuristic could not parse one from the (schema-uncertain) failure payload.
        if failure_event:
            if verification and verification.get("success") is not False:
                verification["success"] = False
            if not failure:
                failure = {
                    "kind": "tool-failure",
                    "summary": redact(command, 240) or "tool reported a failure",
                    "baseline": "uncertain",
                }
    
        def apply(ledger):
            if kinds:
                ledger["changed_files_seen"] = True
                add_unique(ledger, "change_kinds", kinds)
                add_unique(ledger, "changed_paths", [path.strip() for path in paths if path])
            if verification:
                verification["coverage_relation"] = verification_coverage(command, ledger.get("changed_paths", []))
                ledger["verification_results"].append(verification)
                if command:
                    ledger["verification_commands"].append(verification["command"])
                coverage_order = {"none": 0, "uncertain": 1, "generic": 2, "direct": 3}
                current = ledger.get("coverage_relation") or "none"
                observed = verification.get("coverage_relation") or "uncertain"
                if coverage_order.get(observed, 0) > coverage_order.get(current, 0):
                    ledger["coverage_relation"] = observed
            if failure:
                ledger["failures"].append(failure)
    
        update_ledger(input_data, apply)
    
        if failure:
            emit_json(
                {
                    "hookSpecificOutput": {
                        "hookEventName": event,
                        "additionalContext": "fable-ish observed a tool failure. Do not report completion until it is fixed, isolated as baseline, or explicitly documented.",
                    }
                }
            )
        else:
            emit_json({})
        return 0
    
    
    if __name__ == "__main__":
        try:
            raise SystemExit(main())
        except Exception as exc:
            emit_json({"systemMessage": f"fable-ish post-tool hook failed open: {exc}"})
            raise SystemExit(0)
    
  • hooks/pre_tool_use.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Pre-tool guardrails for fable-ish."""
    
    from __future__ import annotations
    
    import sys
    from pathlib import Path
    
    ROOT = Path(__file__).resolve().parents[1]
    sys.path.insert(0, str(ROOT / "scripts"))
    
    from classify_task import classify_tool_risk
    from ledger import add_unique, emit_json, read_stdin_json, update_ledger
    
    
    def deny(reason: str) -> dict:
        return {
            "hookSpecificOutput": {
                "hookEventName": "PreToolUse",
                "permissionDecision": "deny",
                "permissionDecisionReason": reason,
            },
            "systemMessage": reason,
        }
    
    
    def main() -> int:
        input_data = read_stdin_json()
        blocked, flags, reason = classify_tool_risk(input_data)
        if flags:
            update_ledger(input_data, lambda ledger: add_unique(ledger, "risk_flags", flags))
        if blocked:
            emit_json(deny(f"fable-ish blocked tool use: {reason}"))
        else:
            emit_json({})
        return 0
    
    
    if __name__ == "__main__":
        try:
            raise SystemExit(main())
        except Exception as exc:
            emit_json({"systemMessage": f"fable-ish pre-tool hook failed open: {exc}"})
            raise SystemExit(0)
    
  • hooks/stop_gate.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Stop-time completion gate for fable-ish."""
    
    from __future__ import annotations
    
    import contextlib
    import io
    import sys
    from pathlib import Path
    
    ROOT = Path(__file__).resolve().parents[1]
    sys.path.insert(0, str(ROOT / "scripts"))
    
    from ledger import emit_json, load_ledger, read_stdin_json, save_ledger
    from verify_state import should_block_stop, stated_but_unstarted, warning_after_max_blocks
    
    
    FAIL_OPEN_PREFIX = (
        "fable-ish plugin bookkeeping/output issue; failed open. "
        "This is a plugin issue, not evidence that your verification failed:"
    )
    
    
    def failure_payload(exc: Exception) -> dict[str, object]:
        return {"systemMessage": f"{FAIL_OPEN_PREFIX} {exc}"}
    
    
    def main() -> dict[str, object]:
        input_data = read_stdin_json()
        if input_data.get("stop_hook_active") is True:
            # Emit nothing: any additionalContext here would re-open the turn and loop forever.
            return {}
    
        if stated_but_unstarted(str(input_data.get("transcript_path") or "")):
            return {
                "decision": "block",
                "reason": "fable-ish: the previous response only stated an intent to do work without doing it. "
                "Carry it out now with tool calls; end the turn only when the task is complete or you need input "
                "that only the user can provide.",
            }
    
        ledger = load_ledger(input_data)
        block, reason = should_block_stop(ledger)
        if block:
            ledger["stop_blocks"] = int(ledger.get("stop_blocks") or 0) + 1
            save_ledger(input_data, ledger)
            return {"decision": "block", "reason": reason}
    
        warning = warning_after_max_blocks(ledger)
        if warning:
            return {
                "systemMessage": warning,
                "hookSpecificOutput": {
                    "hookEventName": "Stop",
                    "additionalContext": warning,
                },
            }
        return {}
    
    
    def run() -> int:
        captured_stdout = io.StringIO()
        try:
            with contextlib.redirect_stdout(captured_stdout):
                payload = main()
        except Exception as exc:
            payload = failure_payload(exc)
        emit_json(payload)
        return 0
    
    
    if __name__ == "__main__":
        raise SystemExit(run())
    
  • hooks/user_prompt_submit.pyRunsGitHub
    Read the script
    #!/usr/bin/env python3
    """Classify incoming Claude Code prompts for fable-ish."""
    
    from __future__ import annotations
    
    import sys
    from pathlib import Path
    
    ROOT = Path(__file__).resolve().parents[1]
    sys.path.insert(0, str(ROOT / "scripts"))
    
    from classify_task import classify_prompt, context_for_mode
    from ledger import add_unique, emit_json, load_ledger, read_stdin_json, update_ledger
    
    
    CONTINUATION_PREFIXES = (
        "fable-ish: run ",
        "fable-ish: add ",
        "fable-ish: resolve ",
    )
    
    
    def main() -> int:
        input_data = read_stdin_json()
        prompt = str(input_data.get("prompt") or "")
        normalized_prompt = prompt.lstrip().lower()
        if normalized_prompt.startswith(CONTINUATION_PREFIXES):
            ledger = load_ledger(input_data)
            mode = str(ledger.get("task_mode") or "normal")
            risks = list(ledger.get("risk_flags") or [])
            emit_json(
                {
                    "hookSpecificOutput": {
                        "hookEventName": "UserPromptSubmit",
                        "additionalContext": context_for_mode(mode, risks),
                    }
                }
            )
            return 0
    
        mode, risks, goal = classify_prompt(prompt)
    
        def apply(ledger):
            ledger["task_mode"] = mode
            ledger["goal"] = goal
            ledger["changed_files_seen"] = False
            ledger["changed_paths"] = []
            ledger["change_kinds"] = []
            ledger["risk_flags"] = []
            ledger["verification_commands"] = []
            ledger["verification_results"] = []
            ledger["coverage_relation"] = "none"
            ledger["failures"] = []
            ledger["stop_blocks"] = 0
            add_unique(ledger, "risk_flags", risks)
    
        update_ledger(input_data, apply)
    
        if mode == "blocked":
            emit_json(
                {
                    "decision": "block",
                    "reason": "fable-ish blocked this prompt because it appears to request destructive or secret-exposing action. Narrow the request or provide explicit safe scope.",
                }
            )
            return 0
    
        emit_json(
            {
                "hookSpecificOutput": {
                    "hookEventName": "UserPromptSubmit",
                    "additionalContext": context_for_mode(mode, risks),
                }
            }
        )
        return 0
    
    
    if __name__ == "__main__":
        try:
            raise SystemExit(main())
        except Exception as exc:
            emit_json({"systemMessage": f"fable-ish prompt hook failed open: {exc}"})
            raise SystemExit(0)
    

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.

@chrisryugj
Ships withfable-ish

"fable-ish" = "Fable처럼(-ish)". 관찰 가능한 검증으로 통과하기 전에는, 완료를 인정하지 않는다. fable-ish는 Anthropic이 최근 선보인 코딩 모델 Fable이 보여준 검증 규율을 Claude Code 훅으로 재현한 플러그인입니다.

Stars on GitHub

Momentum

stars in total
41
Star history of chrisryugj/fable-ish 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 fable-ish.

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