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.
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.
Claude Code
> /plugin marketplace add jamditis/claude-skills-journalism
Cursor
Cursor also runs Claude Code hooks from .claude/settings.json, so copy these hooks there to use them in Cursor.
Where it lives
#!/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*#!/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.
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.
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.
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.
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.
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.
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.
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.
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.
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.