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
35957 skills1 agent22 commands1 hook
Install
> /plugin marketplace add jamditis/claude-skills-journalism

Ships with claude-skills-journalism. Installing the plugin gets these hooks.

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*

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.

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.

Get the whole plugin