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.
Install
> /plugin marketplace add jamditis/claude-skills-journalismShips 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
Stats
359
Stars
61
Forks
Active
Maintenance
Python
Language
MIT
License
8d ago
Last commit
7mo ago
Created
Repo: jamditis/claude-skills-journalism

