Skip to content
Content
Hook

Hooks

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

From plugin
claude-skills-journalism
45172 skills1 agent22 commands2 hooks

Claude Code

> /plugin marketplace add jamditis/claude-skills-journalism

Where it lives

  • hooks/no-ai-attribution.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """PreToolUse(Bash) hook: block AI authorship credit before it enters the record.
    
    Reads the Claude Code PreToolUse payload on stdin. When the Bash command is a
    `git commit`, `gh pr create`, `gh pr edit`, `gh pr comment`, `gh issue comment`,
    or `gh pr review`, it inspects the text that command would write into the project
    record (commit message, PR/issue body or title, comment or review body, and any
    file those name) plus the commit's author and trailer fields, and denies (exit 2,
    reason on stderr) if it finds a tool/model authorship credit. Anything else exits
    0 and passes straight through.
    
    This is the executable behind hooks/no-ai-attribution.md. It closes the four
    flag-level bypasses that the markdown spec could only describe: git commit
    --author / --trailer, gh pr create --template, and the whole gh pr edit subcommand.
    
    Two detectors, applied to different surfaces, because the surfaces differ in kind:
    - Free prose (message / body / title / named-file contents) legitimately names
      tools as the subject of the work ("document the Claude API client"), so it uses
      the nuanced contains_attribution: an attribution phrase, emoji, or Co-Authored-By
      trailer, NOT a bare mention.
    - Authorship fields (git commit --author, --trailer, and a Co-Authored-By value)
      are authorship by definition, so they use the strict value_names_tool: any
      model/tool token or bot email blocks.
    
    Fails open on anything it cannot read (malformed payload, unbalanced quotes it
    cannot tokenize, an unreadable named file): it never wedges a session, but it
    never silently disables itself either -- on an internal error it says so on stderr,
    then allows.
    """
    import json
    import os
    import re
    import shlex
    import stat
    import sys
    from pathlib import Path
    
    HOOK_NAME = "no-ai-attribution"
    ROBOT_EMOJI = "\U0001F916"  # the sign-off emoji used as an AI byline
    
    # Tokens that name an AI tool, model, or vendor. Used with word boundaries. These are
    # the strict set: any of them in an author/trailer field blocks, because none is a common
    # human name (a person is not named "Anthropic" or "Windsurf"). Coding agents that write
    # commits (aider, windsurf) belong here; agents whose names collide with human names
    # (devin, cline) do not -- they go in the byline-only set below.
    _TOOL_TOKENS = (
        r"claude|chatgpt|gpt(?:-?[0-9][a-z0-9]*)?|codex|copilot|gemini|anthropic|openai|"
        r"cursor|bard|llama|language model|large language model|llm|aider|windsurf"
    )
    # Agent names that are also common human names (Devin, Cline). They credit an AI only
    # inside a byline (an attribution verb tied to the name by with/by), never as a bare
    # author-field token, so a real person named Devin Smith still authors a commit. Kept out
    # of _TOOL_TOKENS for exactly that reason.
    _BYLINE_ONLY_TOKENS = r"devin|cline"
    # Attribution verbs that, paired with with/by and a tool token, form a byline.
    _ATTR_VERBS = (
        r"generated|created|written|authored|made|built|produced|assisted|co-authored|"
        r"powered"
    )
    # Byline tool tokens: the strict tools, the human-name-colliding agents, plus a standalone
    # "AI", so a generic-AI credit ("Generated with AI", "Written by AI") blocks. The negative
    # lookahead keeps it to a bare "AI" token and out of the adjective "AI-<noun>" of subject
    # matter ("Built with AI-powered search"), which is not a byline. Kept separate from
    # _TOOL_TOKENS so the strict author-field check (value_names_tool) does not trip on a human
    # named "Ai" or "Devin".
    _BYLINE_TOOL_TOKENS = _TOOL_TOKENS + r"|" + _BYLINE_ONLY_TOKENS + r"|ai(?![\w-])"
    # Optional "AI-"/"AI " prefix on the attribution verb, so "AI-assisted by Claude" and
    # "AI-generated with GPT" anchor as bylines like the bare verb forms. It only adds a match
    # that also carries a with/by/using/via + named tool, so "add AI-assisted analysis" (no
    # trailing tool credit) still reads as subject matter and passes.
    _AI_VERB_PREFIX = r"(?:ai[-\s])?"
    
    # Leading sign-off / list markers a byline can ride at the start of a line: whitespace,
    # blockquote/comment/bullet markers (`> `, `# `, `* `, `+ `, `• `), and an optional
    # ordered-list number ("1.", "2)"). A credit line in a PR body or changelog often sits
    # behind one of these. The class holds no newline, and the ordered-list group is bounded,
    # so anchoring it under re.M stays linear per line (no newline-crossing quadratic scan).
    _BYLINE_LEAD = r"[ \t>*•:#_=+-]*(?:\d{1,3}[.)][ \t]*)?"
    # The same lead as a standalone anchor, to peel the markers off the front of one line
    # before testing whether a robot emoji leads it. A `- 🤖` or `> 🤖` sign-off rides these
    # markers exactly as the text bylines do, so the emoji check must strip them too.
    _BYLINE_LEAD_RE = re.compile(r"^" + _BYLINE_LEAD)
    # "AI-generated"/"AI-assisted" as a byline: a line whose whole content is the tag
    # (optionally with sign-off punctuation), not the phrase embedded in a sentence. This
    # blocks a standalone "AI-assisted" sign-off but allows "detect AI-generated images",
    # which is subject matter, not authorship. re.M so ^ and $ bind per line. The leading
    # and trailing classes use space/tab, not \s: under re.M an \s* that can eat newlines
    # scans across every line from each ^, which is quadratic on blank-line-heavy input.
    _AI_BYLINE_RE = re.compile(
        r"(?im)^" + _BYLINE_LEAD + r"ai[-\s]?(?:assisted|generated|authored|written)\b[ \t.!)*_-]*$"
    )
    # The same phrase anywhere on a line, used only to tell a robot-emoji byline from a
    # decorative or subject-matter emoji (an emoji sharing a line with this phrase is a
    # byline; one embedded in ordinary prose is not).
    _AI_INLINE_RE = re.compile(r"\bai[-\s]?(?:assisted|generated|authored|written)\b", re.I)
    # The leading class is space/tab, not \s: under re.M an ^\s* eats newlines from every
    # line anchor and, on input with no matching trailer, rescans O(n^2) (a blank-line-heavy
    # clean message hangs the hook). A trailer line has only spaces or tabs before it anyway.
    _COAUTHOR_LINE_RE = re.compile(r"^[ \t]*co-authored-by\s*
  • hooks/prose-question-guard.pyGitHub
    Read the script
    #!/usr/bin/env python3
    """Stop hook: nudge Claude to use AskUserQuestion instead of prose questions.
    
    Use this for interactive sessions where questions belong in AskUserQuestion.
    When the final reply of a turn asks the user something in prose, block the stop
    once and tell Claude to put the question in the tool. A second stop in the
    same chain (stop_hook_active) always passes, so this cannot loop.
    """
    import json
    import os
    import re
    import sys
    
    ASK_PATTERNS = [
        r"\?(?=[\s*_)\]\"']|$)",
        r"\b(let me know|tell me (if|whether|which|when)|your call|up to you)\b",
        r"\b(would you like|do you want|want me to|should i|shall i|can you confirm|ok to|okay to)\b",
        r"\bneeds? your (approval|decision|ok|go-ahead|input|answer)\b",
        r"\b(if you('d| would) (like|prefer|rather))\b",
    ]
    ASK_RE = re.compile("|".join(ASK_PATTERNS), re.IGNORECASE)
    
    
    def strip_code(text):
        text = re.sub(r"(```|~~~).*?\1", "", text, flags=re.DOTALL)
        text = re.sub(r"`[^`]*`", "", text)
        text = re.sub(r"https?://\S+", "", text)
        return re.sub(r"[\"\u201c][^\"\u201d\n]*[\"\u201d]", "", text)  # quoted phrases
    
    
    def is_genuine_user(entry):
        if entry.get("type") != "user":
            return False
        message = entry.get("message")
        content = message.get("content") if isinstance(message, dict) else None
        if isinstance(content, str):
            return True
        if isinstance(content, list):
            return not any(isinstance(b, dict) and b.get("type") == "tool_result" for b in content)
        return False
    
    
    def final_reply(entries):
        """Return (final text, used AskUserQuestion this turn)."""
        turn = []
        for entry in reversed(entries):
            if is_genuine_user(entry):
                break
            turn.append(entry)
        turn.reverse()
        used_auq = False
        texts = []
        for entry in turn:
            if entry.get("type") != "assistant":
                continue
            message = entry.get("message")
            content = message.get("content") if isinstance(message, dict) else None
            for block in content if isinstance(content, list) else []:
                if not isinstance(block, dict):
                    continue
                if block.get("type") == "tool_use":
                    # Only the reply after the last tool call counts; it is exempt
                    # only when that last call was the question tool itself.
                    used_auq = block.get("name") == "AskUserQuestion"
                    texts = []
                elif block.get("type") == "text":
                    texts.append(block.get("text", ""))
        return "\n".join(texts), used_auq
    
    
    def find_question(text):
        for line in strip_code(text).splitlines():
            stripped = line.strip()
            if stripped.startswith(">"):
                continue  # quoted text, not our own question
            if stripped and ASK_RE.search(stripped):
                return stripped[:160]
        return None
    
    
    def main():
        try:
            data = json.load(sys.stdin.buffer)
        except Exception:
            return
        if not isinstance(data, dict):
            return
        if data.get("stop_hook_active"):
            return
        # SDK entrypoints are non-interactive and have no one to ask.
        if os.environ.get("CLAUDE_CODE_ENTRYPOINT", "cli").startswith("sdk"):
            return
        path = data.get("transcript_path")
        entries = []
        if isinstance(path, str) and path:
            try:
                with open(path, encoding="utf-8", errors="replace") as handle:
                    for line in handle:
                        try:
                            entry = json.loads(line)
                        except ValueError:
                            continue
                        if isinstance(entry, dict):
                            entries.append(entry)
            except OSError:
                pass
        text, used_auq = final_reply(entries)
        # Current clients include the final text directly; their transcript can lag.
        if isinstance(data.get("last_assistant_message"), str):
            text = data["last_assistant_message"]
        if used_auq or not text:
            return
        hit = find_question(text)
        if not hit:
            return
        print(json.dumps({
            "decision": "block",
            "reason": (
                "Your final reply seems to ask the user something in prose: \"" + hit + "\". "
                "Use AskUserQuestion for questions that need the user's answer. "
                "If it is a question or a decision for the user, ask it with AskUserQuestion now. "
                "If it is not a question, end the turn again without change."
            ),
        }))
    
    
    if __name__ == "__main__":
        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.

@jamditis
Ships withclaude-skills-journalism

A collection of Agent Skills for journalists, researchers, academics, media professionals, and communications practitioners. The same repository serves Claude Code and Codex while keeping Claude-only commands, agents, and hooks clearly labeled.

45172 skills1 agent
Get the whole plugin

Stars on GitHub

Momentum

stars in total
451
Star history of jamditis/claude-skills-journalism on GitHub, a chart by star-history.com

More plugins that ship hooks.

watch

watch

Lets Claude watch a video from a URL or local file by pulling timestamped frames and a transcript. Works in Claude Code, and the watch skill downloads with yt-dlp and extracts frames with ffmpeg.

1 skill1 hook20k
@bradautomates@bradautomates
Install Plugin
meigen-ai-design-mcp

meigen-ai-design-mcp

Gives Claude Code users access to MeiGen for discovering inspiration and creating images and videos with AI models. It also offers skills for product photoshoot planning, social video thumbnails, and visual creative direction.

3 skills+41.8k
@jau123@jau123
Install Plugin
capcut-cli

capcut-cli

Edits CapCut and JianYing video projects from the terminal or an LLM agent, covering subtitles, timing, speed, volume, templates, animations, and cutting long videos into shorts. The result opens in CapCut with every track still editable.

1 skill1 hook1.1k
@renezander030@renezander030
Install Plugin
wind-comic

wind-comic

Turns a single line of story text into a finished short drama, covering the script, storyboards, character-consistent video and voiceover. Works with the AI model providers you already use, so you skip assembling each step by hand.

7 skills11 hooks704
@chrischen667788@chrischen667788
Install Plugin
bitwize-music

bitwize-music

A Claude Code plugin for planning and producing AI-generated music albums, with skills for album concepts, tracklists, progress dashboards, idea tracking, and album artwork prompts.

53 skills+2648
@bitwize-music-studio@bitwize-music-studio
Install Plugin
master-skill

master-skill

A skill that builds AI teaching personas of specific Buddhist masters, grounded in Buddhist scripture texts and tested for fidelity to their teachings. It includes ethics gates for boundaries on what the persona will say.

21 skills1 hook495
@xr843@xr843
Install Plugin
showtime

showtime

A local video studio for your coding agent. Describe a video in one sentence: your agent directs, your machine renders. Claude Code, Codex, Cursor, Devin and more. No API keys, no uploads.

1 skill+2310
@faviovazquez@faviovazquez
Install Plugin
claude-canvas

claude-canvas

Creates and lays out Obsidian Canvas files as presentations, flowcharts, mind maps, mood boards, and knowledge graphs. It can also fill canvases with images, notes, and diagrams, generate content with AI, and export the results to PNG, SVG, or PDF.

8 skills+3302
@agricidaniel@agricidaniel
Install Plugin

Questions about the hooks in claude-skills-journalism.

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