Hook Development for Claude Code Plugins
anthropics/claude-plugins-official
Explains how to write Claude Code plugin hooks, both prompt-based checks and bash commands, for events such as PreToolUse, Stop and SessionStart.
Guide for writing Datasette plugins. An agent skill from simonw/research.
$ npx skills add simonw/research --skill datasette-plugin-writer -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install simonw/research datasette-plugin-writer --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/simonw/research.git skills-src && mkdir -p .claude/skills && cp -r skills-src/datasette-plugin-skill .claude/skills/datasette-plugin-writer && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "datasette-plugin-writer" agent skill from https://github.com/simonw/research/tree/main/datasette-plugin-skill into .claude/skills/datasette-plugin-writer/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "datasette-plugin-writer", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/simonw/research/tree/main/datasette-plugin-skillType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add simonw/research --skill datasette-plugin-writer -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install simonw/research datasette-plugin-writer --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simonw/research.git skills-src && mkdir -p .agents/skills && cp -r skills-src/datasette-plugin-skill .agents/skills/datasette-plugin-writer && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "datasette-plugin-writer" agent skill from https://github.com/simonw/research/tree/main/datasette-plugin-skill into .agents/skills/datasette-plugin-writer/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "datasette-plugin-writer", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add simonw/research --skill datasette-plugin-writer -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install simonw/research datasette-plugin-writer --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simonw/research.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/datasette-plugin-skill .cursor/skills/datasette-plugin-writer && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "datasette-plugin-writer" agent skill from https://github.com/simonw/research/tree/main/datasette-plugin-skill into .cursor/skills/datasette-plugin-writer/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "datasette-plugin-writer", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/simonw/research.git --path datasette-plugin-skill--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add simonw/research --skill datasette-plugin-writer -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install simonw/research datasette-plugin-writer --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simonw/research.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/datasette-plugin-skill .gemini/skills/datasette-plugin-writer && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "datasette-plugin-writer" agent skill from https://github.com/simonw/research/tree/main/datasette-plugin-skill into .gemini/skills/datasette-plugin-writer/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "datasette-plugin-writer", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install simonw/research datasette-plugin-writerInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add simonw/research --skill datasette-plugin-writer -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/simonw/research.git skills-src && mkdir -p .github/skills && cp -r skills-src/datasette-plugin-skill .github/skills/datasette-plugin-writer && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "datasette-plugin-writer" agent skill from https://github.com/simonw/research/tree/main/datasette-plugin-skill into .github/skills/datasette-plugin-writer/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "datasette-plugin-writer", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add simonw/research --skill datasette-plugin-writer -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install simonw/research datasette-plugin-writer --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/simonw/research.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/datasette-plugin-skill .opencode/skills/datasette-plugin-writer && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "datasette-plugin-writer" agent skill from https://github.com/simonw/research/tree/main/datasette-plugin-skill into .opencode/skills/datasette-plugin-writer/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "datasette-plugin-writer", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
datasette-plugin-writerGuide for writing Datasette plugins. An agent skill from simonw/research.
Datasette Plugin Writer is an agent skill from simonw/research. Guide for writing Datasette plugins. This skill should be used when users want to create or develop plugins for Datasette, including information about plugin hooks, the cookiecutter template, database APIs, request/response handling, and plugin configuration.
Its SKILL.md is about 2.9k tokens, which your agent loads only when the skill is triggered. The skill folder holds 2 other files (for example `README.md` and `_summary.md`).
The licence is Apache-2.0.
Read from SKILL.md and the folder at commit 4f5f23e. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
gituvxpythonpipFrom the folder's file list and the shell code blocks in SKILL.md.
Links to these hosts (documentation or services it may open):
docs.datasette.iodatasette.iogithub.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Datasette Plugin Writer loads about 2.9k tokens when it runs. Until then it costs about 71 tokens; SKILL.md has 405 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from simonw/research at commit 4f5f23e, republished under its Apache-2.0 licence (© simonw). 405 words, ~2,946 tokens.
.claude/skills/datasette-plugin-writer/SKILL.md (or your agent's skills folder). This skill also uses 2 other files; get the full folder from GitHub.Use this skill to build plugins for Datasette, the open source multi-tool for exploring and publishing data.
Start a new plugin using the datasette-plugin cookiecutter template with newline-delimited variables:
echo "plugin_name
description of plugin
plugin-hyphenated-name
plugin_underscored_name
github_username
Author Name
y
y" | uvx cookiecutter gh:simonw/datasette-pluginExample for a plugin called "my-cool-plugin":
echo "my cool plugin
A plugin that does cool things
my-cool-plugin
my_cool_plugin
username
Your Name
y
y" | uvx cookiecutter gh:simonw/datasette-pluginThe last two y responses enable static/ and templates/ directories.
After creating the plugin:
cd datasette-my-cool-plugin
python -m venv venv
source venv/bin/activate
pip install -e '.[test]'
datasette plugins # Verify plugin is visible
python -m pytest # Run testsA typical plugin structure:
datasette-my-plugin/
├── datasette_my_plugin/
│ ├── __init__.py # Plugin hooks go here
│ ├── static/ # Optional: CSS, JavaScript
│ └── templates/ # Optional: Custom templates
├── tests/
│ └── test_my_plugin.py
├── setup.py or pyproject.toml
└── README.mdRegister custom SQL functions. Called when SQLite connections are created:
from datasette import hookimpl
@hookimpl
def prepare_connection(conn):
conn.create_function("hello_world", 0, lambda: "Hello world!")Add custom URL routes. Return list of (regex, view_function) pairs:
from datasette import hookimpl, Response
async def my_page(request):
return Response.html("<h1>Hello!</h1>")
@hookimpl
def register_routes():
return [
(r"^/-/my-page$", my_page)
]View functions can accept: datasette, request, scope, send, receive.
Customize how table cell values are displayed:
from datasette import hookimpl
import markupsafe
@hookimpl
def render_cell(value, column):
if column == "stars":
return markupsafe.Markup("⭐" * int(value))Add variables to template context:
@hookimpl
def extra_template_vars(request, datasette):
return {
"user_agent": request.headers.get("user-agent"),
"custom_data": "value"
}Can also return async functions for database queries.
Add menu items to table pages:
@hookimpl
def table_actions(datasette, database, table):
return [{
"href": datasette.urls.path(f"/-/export/{database}/{table}"),
"label": "Export this table",
"description": "Download as CSV"
}]Implement authentication. Return actor dict or None:
@hookimpl
def actor_from_request(request):
token = request.args.get("_token")
if token == "secret":
return {"id": "user123", "name": "Alice"}Can return async function for database lookups.
Control permissions. Return True (allow), False (deny), or None (no opinion):
@hookimpl
def permission_allowed(actor, action, resource):
if action == "execute-sql" and actor and actor.get("id") == "admin":
return TrueAvailable in many plugin hooks:
request.method # "GET" or "POST"
request.url # Full URL
request.path # Path without query string
request.full_path # Path with query string
request.query_string # Query string without ?
request.args # MultiParams object for query params
request.args.get("key") # Get single param value
request.args.getlist("key") # Get list of values
request.headers # Dict of headers (lowercase keys)
request.cookies # Dict of cookies
request.actor # Current authenticated actor or None
request.url_vars # Variables from URL regex
# Async methods:
body = await request.post_body() # Raw POST body as bytes
form_vars = await request.post_vars() # Form data as dictCreate responses in view functions:
from datasette.utils.asgi import Response
# HTML response
return Response.html("<h1>Hello</h1>")
# JSON response
return Response.json({"status": "ok"})
# Text response
return Response.text("Plain text")
# Redirect
return Response.redirect("/other-page")
# Custom response
return Response(
body="Content",
status=200,
headers={"X-Custom": "value"},
content_type="text/plain"
)
# Set cookies
response = Response.html("<h1>Hello</h1>")
response.set_cookie("session", datasette.sign({"id": "123"}, "cookie"))Access databases in plugins:
# Get database object
db = datasette.get_database("mydb") # Named database
db = datasette.get_database() # First database
# Execute read query
results = await db.execute("SELECT * FROM mytable WHERE id = ?", [123])
for row in results:
print(row["column_name"])
# Query properties
results.rows # List of Row objects
results.columns # List of column names
results.truncated # True if results were truncated
results.first() # First row or None
results.single_value() # Single value if query returned one
# Execute write query
await db.execute_write(
"INSERT INTO mytable (name) VALUES (?)",
["value"]
)
# Execute multiple writes
await db.execute_write_many(
"INSERT INTO mytable (id, name) VALUES (?, ?)",
[(1, "Alice"), (2, "Bob")]
)
# Execute function with write connection
def insert_and_count(conn):
conn.execute("INSERT INTO mytable (name) VALUES (?)", ["Alice"])
return conn.execute("SELECT COUNT(*) FROM mytable").fetchone()[0]
count = await db.execute_write_fn(insert_and_count)
# Introspection
tables = await db.table_names()
views = await db.view_names()
columns = await db.table_columns("mytable")
exists = await db.table_exists("mytable")Users configure plugins in datasette.yaml:
plugins:
datasette-my-plugin:
api_key: secret123
enabled: trueOr per-database:
databases:
mydb:
plugins:
datasette-my-plugin:
setting: valueAccess in plugin code:
config = datasette.plugin_config("datasette-my-plugin")
api_key = config.get("api_key") if config else None
# With database/table context
config = datasette.plugin_config(
"datasette-my-plugin",
database="mydb",
table="mytable"
)Configuration lookup: table → database → instance level.
Place in static/ directory, reference with:
# In Python
url = datasette.urls.static_plugins("datasette_my_plugin", "app.js")
# In templates
<script src="{{ urls.static_plugins('datasette_my_plugin', 'app.js') }}"></script>Place in templates/ directory. Override Datasette templates:
database.html - Database pagetable.html - Table pagerow.html - Row pagequery.html - Query pageAccess template functions:
{{ csrftoken() }} {# CSRF token for forms #}
{{ urls.instance() }} {# Homepage URL #}
{{ urls.database("mydb") }} {# Database URL #}
{{ urls.table("mydb", "mytable") }} {# Table URL #}@hookimpl
def prepare_connection(conn):
import hashlib
def md5(text):
return hashlib.md5(text.encode()).hexdigest()
conn.create_function("md5", 1, md5)@hookimpl
def register_routes(datasette):
async def stats_page(request):
db = datasette.get_database()
tables = await db.table_names()
return Response.html(
await datasette.render_template(
"stats.html",
{"tables": tables},
request=request
)
)
return [(r"^/-/stats$", stats_page)]@hookimpl
def register_output_renderer(datasette):
def render_csv(columns, rows):
import csv, io
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(columns)
writer.writerows(rows)
return Response(
output.getvalue(),
content_type="text/csv"
)
return {
"extension": "csv",
"render": render_csv
}@hookimpl
def register_routes(datasette):
async def admin_page(request):
# Check if user has permission
allowed = await datasette.permission_allowed(
request.actor,
"admin-page",
default=False
)
if not allowed:
from datasette import Forbidden
raise Forbidden("Admin access required")
return Response.html("<h1>Admin Page</h1>")
return [(r"^/-/admin$", admin_page)]Use /-/ prefix to avoid conflicts with database names:
/-/my-plugin - Instance-level page/dbname/-/my-plugin - Database-level page/dbname/table/-/my-plugin - Table-level pageBuild URLs with base_url support:
datasette.urls.path("/-/my-page")
datasette.urls.database("mydb")
datasette.urls.table("mydb", "mytable")Create tests in tests/test_my_plugin.py:
from datasette.app import Datasette
import pytest
@pytest.mark.asyncio
async def test_my_plugin():
datasette = Datasette()
await datasette.invoke_startup()
# Test with client
response = await datasette.client.get("/-/my-page")
assert response.status_code == 200
# Test database operations
db = datasette.get_database()
result = await db.execute("SELECT hello_world()")
assert result.first()[0] == "Hello world!"Run tests: pytest
git init
git add .
git commit -m "Initial commit"
git branch -m main
git remote add origin git@github.com:username/datasette-my-plugin.git
git push -u origin mainConfigure GitHub release environment with PyPI trusted publisher, then create a GitHub release matching your version number. The GitHub Action will automatically publish to PyPI.
@hookimpl decorator for all plugin hooksasync def and await<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">- prefix in URLs: /-/plugin-pathdatasette.get_internal_database()© simonw, Apache-2.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
SKILL.md and 2 other files in datasette-plugin-skill of simonw/research.
Open the folder on GitHubat commit 4f5f23e
Datasette Plugin Writer next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Datasette Plugin Writer this skillsimonw/research | 783 | — | ~2.9k | Automated safety check: Pass | Apache-2.0 | |
| Hook Development for Claude Code Pluginsanthropics/claude-plugins-official | 38k | 11 repos | ~4.1k | Automated safety check: Notes | Apache-2.0 | |
| Claude Code Agent Developmentanthropics/claude-plugins-official | 38k | 8 repos | ~2.8k | Automated safety check: Pass | Apache-2.0 | |
| Claude Code Skill Developer Guidediet103/claude-code-infrastructure-showcase | 10k | 11 repos | ~3.5k | Automated safety check: Pass | MIT | |
| Plugin Settings Patternanthropics/claude-plugins-official | 38k | 7 repos | ~3k | Automated safety check: Pass | Apache-2.0 | |
| MCP Integration for Pluginsanthropics/claude-plugins-official | 38k | 11 repos | ~3.1k | Automated safety check: Pass | Apache-2.0 |
anthropics/claude-plugins-official
Explains how to write Claude Code plugin hooks, both prompt-based checks and bash commands, for events such as PreToolUse, Stop and SessionStart.
anthropics/claude-plugins-official
Explains how to write agents for Claude Code plugins: the markdown file with YAML frontmatter, trigger descriptions, model and color settings, and system prompt design.
diet103/claude-code-infrastructure-showcase
A guide to creating and managing Claude Code skills with auto-activation: skill-rules.json triggers, hooks, enforcement levels, YAML frontmatter and progressive disclosure.
anthropics/claude-plugins-official
Shows how Claude Code plugins keep per-project settings and state in .claude/plugin-name.local.md files with YAML frontmatter and a markdown body.
anthropics/claude-plugins-official
Explains how to bundle Model Context Protocol servers in a Claude Code plugin, covering config files, stdio, SSE, HTTP and WebSocket server types, and authentication.
fossasia/eventyay-interpretation
Set up Claude Code hooks to block dangerous git commands (push, reset --hard, clean, branch -D, etc.) before they execute.
simonw/research
Build async web applications and APIs with Starlette 1.0, the lightweight ASGI framework for Python.
simonw/research
Summarize a CSV, compute basic stats, and produce a markdown report + a plot image.
Guide for writing Datasette plugins. An agent skill from simonw/research. Datasette Plugin Writer is an agent skill from simonw/research. Guide for writing Datasette plugins.
Datasette Plugin Writer fits situations like: tasks that involve Hooks and plugins.
Run `npx skills add simonw/research --skill datasette-plugin-writer -a claude-code`. Or copy the skill folder (datasette-plugin-skill in simonw/research) into .claude/skills/datasette-plugin-writer in your project. Claude Code loads it when a task matches its description.
Run `npx skills add simonw/research --skill datasette-plugin-writer -a codex`. Or copy the skill folder (datasette-plugin-skill in simonw/research) into .agents/skills/datasette-plugin-writer in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add simonw/research --skill datasette-plugin-writer -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/datasette-plugin-writer, .gemini/skills/datasette-plugin-writer, .github/skills/datasette-plugin-writer and .opencode/skills/datasette-plugin-writer in your project.
Going by SKILL.md and its folder, Datasette Plugin Writer needs the command-line tools its instructions call (git, uvx, python and pip). Our summary lists: Python 3.
SKILL.md names 3 domains. As links in the text: docs.datasette.io, datasette.io and github.com. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Datasette Plugin Writer is published under the Apache-2.0 licence (declared in SKILL.md). It allows redistribution, so the full SKILL.md is shown on this page.
About 2.9k tokens (SKILL.md is roughly 12k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Datasette Plugin Writer: Hook Development for Claude Code Plugins (anthropics/claude-plugins-official, 38k stars), Claude Code Agent Development (anthropics/claude-plugins-official, 38k stars), Claude Code Skill Developer Guide (diet103/claude-code-infrastructure-showcase, 10k stars) and Plugin Settings Pattern (anthropics/claude-plugins-official, 38k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
simonw (a GitHub user) maintains it in simonw/research, which has 783 GitHub stars. The repository holds 3 skills in this directory. The repository was last updated on October 4, 2026.
Source: simonw/research on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.