agentleFS
dgilforddgilford/ai-science-toolkit/CLAUDE.md

ai-science-toolkit

CLAUDE.md82 starsChanged 42 days agoMITView on GitHub

This directory is a workspace for developing and iterating on global Claude Code skills. Skills are installed at ~/.claude/skills/<name>/SKILL.md and are available across all projects. The canonical "when to reach for each" catalog is skills/pathfinder/SKILL.md (/pathfinder); this table is the short index. The external tab-setup skill is excluded here — see Boot hooks and Syncing below.

  • Reads credentials
  • Deletes or force-pushes
  • Commits and pushes

The file

We split this CLAUDE.md into the rules it gives an agent

2 rules. About 0 are common, 0 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    All four share tools: Read, Grep, Glob and model: opus.

    line 56
  2. Very specific:

    After pull, review git diff skills/ agents/ — pull brings in all globally installed skills and agents, including any not yet tracked in …

    line 179

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Purpose

This directory is a workspace for developing and iterating on global Claude Code skills. Skills are installed at ~/.claude/skills/<name>/SKILL.md and are available across all projects.

Installed skills

The canonical "when to reach for each" catalog is skills/pathfinder/SKILL.md (/pathfinder); this table is the short index. The external tab-setup skill is excluded here — see Boot hooks and Syncing below.

<!-- gen-docs:skills:start (generated by scripts/gen-docs.sh from skills/*/SKILL.md catalog frontmatter — do not hand-edit) -->
SkillTriggerPurpose
repo-init/repo-initScaffold a new repo (or retrofit an existing one) with a standard structure via a short intake grill: research mode by default, --package for a distributable library. Never overwrites; --dry-run previews.
handoff/handoffCreate or update a durable project handoff (.ai/HANDOFF.md) for the next AI agent/session.
worklog/worklogLog a work entry to the Notion Work Journal + remote server cache + local .ai/ mirror — the capture core invoked by /handoff and whenever you ask to log something.
pickup/pickupPick up work from repo-local handoff state — the read side of the handoff pair.
recover-sessions/recover-sessionsFind and resume closed Claude Code sessions from the on-disk transcript store — working directory, subject, and a ready-to-paste resume command.
evolve-claude-md/evolve-claude-mdUpdate CLAUDE.md — or the canonical AGENTS.md it redirects to — with durable knowledge from the current session.
retro/retroRetrospective on a coding session: proposes environment fixes — a check for each repeated mistake, a pointer for each slow lookup, a cut for each no-op instruction — so CLAUDE.md gets shorter over time, not longer. Report-only. Adapted from Matt Pocock.
grill-me/grill-meInterview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Thin launcher for the model-invokable grilling core. By Matt Pocock.
grilling/grillingGrill the user relentlessly about a plan or design — the decision tree worked in rounds along its frontier, facts looked up rather than asked — until shared understanding. The model-invokable core behind /grill-me. Adapted from Matt Pocock.
lit-review/lit-reviewSearch and synthesize scientific literature from Zotero, arxiv, bioRxiv, Google Scholar, and Consensus. Zotero write support needs ZOTERO_* env vars in ~/.claude/settings.json.
overbaked/overbakedAudit a document, plan, or code for over-engineering, verbosity, and scope creep.
briefly/brieflyCap the length and detail of a reply — the answer only, sized to the question. Works mid-prompt.
slack-message/slack-messageDraft an internal Slack message grounded in current project context and recent workflow.
disclose/discloseEstimate the AI spectrum of involvement (0–10, after MediaWise/Poynter) for a shareable deliverable, have the user confirm or revise it, and emit a content-disclosure line naming the model's role. Logs to .ai/disclosures.jsonl.
disclose-project/disclose-projectProject-level companion to disclose: interviews you phase by phase (design → data → literature → analysis → figures → writing → review) to build a per-phase spectrum-of-involvement table and a publication-ready AI-use statement, primed with the disclose ledger and Claude co-author trailers from git.
create-alert/create-alertAuthor a scheduled Slack alert: grill a "tell me when X happens" ask into a testable trigger, dry-run it live, and — after you sign off on the synthesized spec — create a claude.ai cloud routine that messages Slack when it fires.
write-new-skill/write-new-skillCreate new Claude Code skills with proper structure and progressive disclosure.
unstale/unstaleDetect and repair staleness residue in Python library code and notebooks — dead imports, dead code, resolved TODOs, stale comments/docstrings, and HANDOFF blockers; --auto applies HIGH-confidence fixes.
figure-review/figure-reviewAudit a scientific figure for publication-readiness: colormaps, uncertainty, axes, caption, and claim support; panel-aware, and raises likely-deliberate choices as by-design?; --style adds CC house style.
spot-ai/spot-aiAudit a document, paragraph, or sentence for AI-isms: gray-list style tells (lane 1) and substance weaknesses — hollow claims, fabricated specifics, missing hedges (lane 2). Severity + confidence per finding, fix suggested or missing substance named; per-repo .ai/graylist.md can add, exempt, or re-tune thresholds. Flags only — never rewrites.
reviewer-2/reviewer-2Adopt a critical-reviewer stance to stress-test a claim, result, or manuscript section: baseline, counterfactual, alternatives, uncertainty consistency.
pathfinder/pathfinderRouter: a navigable map of every skill and subagent and when to reach for each; resolves the reviewer-2-vs-panel review decision.
ai-review/ai-reviewComprehensive senior-engineer repo review; orchestrates a parallel fan-out that delegates to code-review/security-review/unstale/overbaked/reviewer-2 and adds gap-hunting, grounded ideation, and prioritized synthesis. Report-only by default; --fix opts into HIGH-confidence unstale repairs.
commit-batch/commit-batchBatch the working tree into logical, single-concern commits, then commit and push if asked. Thin launcher for the model-invokable commit-batching core.
commit-batching/commit-batchingBatch a dirty working tree into logical, single-concern commits (survey → group → stage by path → commit → push if asked) — the model-invokable core behind /commit-batch.
<!-- gen-docs:skills:end -->

Review agents (subagent panel)

Domain-expert reviewer personas live in agents/<name>.md and deploy to ~/.claude/agents/ via sync.sh push. They are invokable subagent types (spawned with the Agent tool / subagent_type), not slash-command skills. Each is review-only — it reports findings (severity-tagged, with a summary table) and never rewrites the target. They are designed to run individually or as a parallel panel (spawn several in one message for a multi-reviewer read).

<!-- gen-docs:agents:start (generated by scripts/gen-docs.sh from agents/*.md catalog frontmatter — do not hand-edit) -->
AgentDomainReviews for
attribution-reviewerClimate attributionCounterfactual, baseline, framing, uncertainty, model adequacy, overclaiming (PR/OR/ChIP, storyline, D&A)
stats-reviewerStatistics / MLEstimator properties, causal ID, inference under dependence, specification, multiple testing, calibration
meteo-reviewerMeteorology (AMS CCM)Dynamical/thermodynamic consistency, physical basis, observational adequacy, competing drivers, hydrology
scicomm-reviewerScience communicationMessage Box, narrative, stakes/framing, audience, quantification (full COMPASS portfolio)
<!-- gen-docs:agents:end -->

All four share tools: Read, Grep, Glob and model: opus.

Agent file format & gotcha

Same YAML frontmatter as skills (name, description, tools, model). Critical: the loader silently drops any agent whose frontmatter fails to parse — the .md deploys fine but the type never registers, and it only surfaces in a fresh session (the type list is snapshotted at session start). The classic failure is an unquoted multi-line description containing ": " (colon-space) on a wrapped line, which YAML reads as a stray mapping key. Always single-quote multi-line descriptions (description: '...'). sync.sh push runs lint_frontmatter() to strict-parse every agent's and skill's frontmatter (and check the description cap) before deploying, and .github/workflows/lint.yml enforces the same in CI. It also runs lint_skill_refs(), which checks that every backticked `/slash-command` resolves to a real skill in skills/ (or an allowlisted Claude Code built-in) — so a launcher like grill-me (whose body is just "Run a /grilling session.") can't silently no-op if its target is renamed or deleted. It scans skill bodies and their companion .md files at any depth, agent files, and the prose docs (CLAUDE.md, README.md, docs/**.md) — renames routinely land in skills/ but not in the prose. Three allowlists, in descending order of preference: SCOPED_REFS for (file, command) pairs where one file has a reason to name a non-skill command (the /resume explanations); BUILTINS for genuine Claude Code built-ins, legitimate anywhere; PLACEHOLDERS for prose stand-ins like `/slash` that aren't commands at all. Prefer SCOPED_REFS — a repo-wide entry disables detection in every other file. A third lint, lint_templates(), runs tests/smoke_repo_init.py on every push/lint (and in CI): templates a skill stamps into other repos (repo-init's TEMPLATES.md) are invisible to the frontmatter/ref lints, so the smoke test parses every fenced toml/yaml/python block, materializes the gitignores in an isolated tmp repo to assert the tracked/ignored contract, and spot-checks known-wrong-but-parseable values (pre-commit hook ids). Two shipped-class bugs (a git-impossible ! allowlist under a directory-form ignore; a hook id that didn't exist at the pinned rev) motivated it — extend the smoke test when adding template-stamping skills.

Skill file format

Each skill lives at ~/.claude/skills/<name>/SKILL.md with YAML frontmatter:

---
name: skill-name
description: shown to Claude for auto-invocation matching
allowed-tools: Bash Read Write Edit
argument-hint: "[--flag | optional arg]"   # shown in user-facing help; optional
disable-model-invocation: true             # user-invoked only; see below
---

Shell commands in ```! blocks run before Claude sees the skill content — use for injecting live repo state (git status, log, diff).

If a skill's logic depends on discovering neighbouring files, allowed-tools must grant Glob/Grep — the failure is silent. Without them the skill cannot list a directory or search sibling files, so a step phrased as "look for related files before deciding" simply never runs, and the skill falls through to whatever it can infer from the single path it was handed. Nothing errors; the discovery step just evaporates. figure-review shipped this way: its whole panel-vs-standalone determination rests on finding sibling panels, while allowed-tools granted only Bash Read Write, which would have reduced the check to a filename guess. Bash alone is not a substitute — ls/grep work, but the skill body has to say to use them, and a body written in terms of "look for" will not.

Invocation control: user-invoked vs model-invokable

disable-model-invocation: true is a supported SKILL.md frontmatter key (verified at code.claude.com/docs/en/skills). It blocks Claude from auto-invoking the skill and prevents preloading into subagents; the /slash command still works — but only when it leads the submission (see "Slash-command expansion is position-sensitive" below). The companion key user-invocable: false does the inverse (hide from the / menu; Claude-only background-knowledge skills).

Non-obvious gotcha: the field does not reclaim description token budget — the description stays in the model's selection context, so the model still sees it and may try to invoke it; only the tool-call is blocked. Don't add this field expecting token savings — add it for invocation control. The only real token lever is keeping the description short, and the lint enforces a cap. Version, evidence tier, and the upstream bug numbers for both facts: docs/harness-behavior.md rows 9 and 11 — the cap in particular is reported, not observed, so re-verify the exact number before relying on it.

Skills carry the field on the user-invoked vs model-invokable axis:

  • User-invoked (disable-model-invocation: true) — orchestrators you type explicitly: grill-me, handoff, pickup, slack-message, create-alert, tab-setup, write-new-skill, pathfinder, ai-review, commit-batch, repo-init, spot-ai, retro, disclose-project.
  • Model-invokable (no field) — Claude or another skill may reach them mid-task: figure-review, grilling, lit-review, overbaked, reviewer-2, unstale, evolve-claude-md, commit-batching, worklog, recover-sessions, disclose, briefly (stays ungated so the model can reach it mid-prompt, but it rarely does — docs/harness-behavior.md row 4e — so the reliable mid-prompt path is the briefly hook).

A gated skill only expands at the head of a submission — and whether the Skill tool can reach it by name is version-dependent, so don't design around either answer. The CLI won't expand it mid-sentence: no <command-name> block, no injected body, so nothing visibly happens. What happens on an explicit Skill(<name>) call has flip-flopped across CLI versions: at 2.1.221 it ran normally (gating seemed to govern only autonomous selection), but at 2.1.224 it hard-rejects with an explicit error naming the field and instructing the model not to replicate the workflow by other means. From 2.1.224, then, a gated skill was reachable only by a user-typed leading /slash-command — and at 2.1.310 a gated skill the user names mid-prompt was callable again (register row 9d, n=1, possibly a narrower user-mentioned pathway). Either way, keep the launcher/core split and don't design around either answer: the gated launcher is what you type, the ungated core is what another skill can reach mid-task. Expansion mechanics (argument capture, what happens with several commands in one submission), their evidence tiers, and the CLI version they were seen on live in docs/harness-behavior.md — that file is the register, so check it before relying on any of them and record new observations there, not here.

Composition rule: a user-invoked skill may invoke model-invokable skills, never another user-invoked one. (handoff → evolve-claude-md and handoff → worklog are why both are model-invokable; grill-me → grilling is the same pattern — grill-me is a thin launcher so the grilling core stays reachable mid-task; repo-init → grilling likewise; slack-message → disclose.) Write a launcher body as an explicit tool call — "Call the Skill tool with grilling (the /grilling core)" — not as a bare mid-sentence /slash, which the CLI never expands and the model only sometimes infers; keep the backticked slash reference alongside so lint_skill_refs() still guards the wiring. The 4 reviewer subagents are out of scope for this field — subagents use their own description-based invocation.

Interactive gates need the full triad. A skill step that must ask the user before acting (repo-init's intake grill is the reference case) will be skipped by a low-effort executor if stated as a plain instruction — observed live: a session default of effortLevel: low blew straight past "ask these questions first" and scaffolded with assumed defaults. The pattern that holds: (1) a hard gate stated up top ("create nothing until…"), (2) an Anti-Rationalization table pre-empting the specific excuses ("dir is empty, nothing to ask", "I can infer defaults"), and (3) a Verification checklist whose first items are the gate. Additionally, user answers must be recorded verbatim (quoted) in the output artifact — a gate-skipping model that invents answers otherwise forges plausible-looking records; quoting makes fabrication checkable against the transcript. One more coupling since the grilling core moved to frontier rounds (2026-10-26): the core asks every unblocked question at once by default, so a gate whose verbatim-answer record depends on one answer per question must pin the cadence in its own body ("one at a time, waiting for each answer") — repo-init does; the core honors a caller-stated cadence.

Skills may include companion reference files (e.g., REFERENCE.md, CC-STYLE.md, COLORBLIND.md) in the same skill directory. Load them at runtime using the same ! block syntax above, pointing at the deployed path: cat ~/.claude/skills/<name>/COMPANION.md 2>/dev/null || echo "(not found)". This keeps SKILL.md lean while injecting richer context at load time. Only the deployed path (~/.claude/skills/) is referenced — the repo copy in skills/ is synced there by sync.sh push.

Catalog tables (generated — don't hand-edit)

The skill/agent tables above, and the ones in README.md, are generated by scripts/gen-docs.sh from a catalog: frontmatter block — this used to be three hand-maintained copies (CLAUDE.md, README.md, and this SKILL.md's own list of skills) that drifted twice. When adding or editing a skill, set catalog: {order: <int>, summary: '<one-liner>'} in its SKILL.md frontmatter (order only sorts CLAUDE.md's table and steps by 10 — 10, 20, 30… — so a new skill slots between two neighbors by taking the gap, e.g. 45, touching one file instead of renumbering the tail) (agents use catalog: {order, domain, reviews_for, summary} instead of a single summary); set claude_md: false to exclude a skill from CLAUDE.md's table (e.g. the external tab-setup skill) and repo_url to render it as a link in README. For third-party work, add a provenance block — {relation: authored|adapted|forked, author, url} — and leave the attribution out of the summary; gen-docs.sh renders the "By / Adapted from / Forked from" suffix from it, so the credit can't drift between the two tables (it did: a skill's summary once said "By" while its description said "Adapted from"). Use authored for a verbatim upstream skill, adapted when you've meaningfully changed it, forked for a maintained fork. Then run bash scripts/gen-docs.sh write and commit the result — .github/workflows/lint.yml runs gen-docs.sh check in CI and fails the build if the committed tables don't match. pathfinder's own "reach for it when" tables are hand-written on purpose (different framing than a one-line summary) and aren't generated; gen-docs.sh doesn't check it.

Shell lint (scripts/lint-shell.sh). ShellCheck -S warning over every tracked *.sh, discovered with git ls-files so a new script is covered as soon as it's committed. It is the single source of truth: both .github/workflows/lint.yml and sync.sh's lint_shell call it, because the target list used to be inlined in the workflow and had already drifted (the step was named "all tracked shell scripts" while omitting settings/statusline-command.sh). Two non-obvious details, both learned by shipping an SC2115 to main: ShellCheck lives at ~/.local/bin/shellcheck (pip shellcheck-py) and is often not on PATH, so the script resolves that path explicitly — the local gate had been silently skipping shell checks while CI enforced them; and the missing-linter path degrades to a warning locally but hard-fails under REQUIRE_SHELLCHECK=1, which CI sets, so a linter that vanishes can never turn into a quiet pass. When adding a rm -rf "$VAR/sub", write "${VAR:?}/sub" — SC2115 fails the build otherwise, and the guard doesn't depend on a reader tracing the enclosing control flow. Know the gate's blind spot: ShellCheck does not flag unquoted expansion in for x in $var, because word-splitting there is usually intended — so it also says nothing about the pathname expansion that comes with it. That is the whole hazard set -f exists to stop in hook-warn-stacked-commands.sh, and it is invisible at -S warning and at -o all (which emits only SC2250/SC2292 style nits). When a script's correctness rests on a line the linter can't see, say so in a comment at the line — for that class of bug the comment is the only guard, and a future refactor will otherwise read the line as redundant.

Hand-maintained user docs: docs/harness-behavior.md is the register for observed Claude Code behavior — undocumented parser/loader/settings internals, each row carrying an evidence tier (observed / inferred / reported), the CLI version it was seen on, and its confounds. Volatile harness facts go there, not here: this file is loaded on every run, and nothing in CI can test a claim about a proprietary CLI. When promoting a consequence out of the register into this file, prefer claims that fail loudly. The one fact promoted here previously ("a gated skill named mid-sentence has no invocation path") was also the only one nobody re-tested — precisely because it read as settled — and it was wrong for weeks until a probe aimed at something else tripped over it (register row 9b). A claim whose failure mode is silence gets no feedback from being stated confidently in an always-loaded file; it just becomes load-bearing. This has now happened three times, and the last two were cheaply testable — the .ai/ tracking-leak claim (corrected 2026-08-07) needed a five-line scratch repo to falsify, and the ANTHROPIC_API_KEY-in-env claim needed one grep of the file it described. So the lesson is not only "prefer claims that fail loudly": when a claim here is about tool behavior you can measure in a minute, measure it rather than reasoning about it — especially when re-reading it during unrelated work, since that is how all three were caught. docs/configuration.md documents each skill's config surface (env vars, connectors, degradation behavior) for external users — nothing generates or lints it, so update it whenever a skill's env vars, required connectors, or no-config fallback behavior change. Same for the skill_deps() map in scripts/sync.sh when skill-to-skill wiring changes (see Syncing skills).

Session workflow

These skills form a session lifecycle:

  • Start: /pickup — loads handoff, reports state, recommends next action
  • End: /handoff — writes .ai/HANDOFF.md, then runs evolve-claude-md
  • Anytime: /evolve-claude-md — standalone CLAUDE.md update
  • Periodically: /retro — the counterweight to evolve-claude-md: report-only proposals to turn a repeated mistake into a check, a slow lookup into a pointer, and a no-op instruction into a cut. evolve-claude-md only adds, and /handoff runs it every session, so without this the file grows without bound (this repo's reached ~9k words before retro existed)
  • Planning: /grill-me — stress-test a design before implementing

The .ai/ directory is repo-local and is gitignored.

Review-report archiving: the seven review skills (ai-review, figure-review, reviewer-2, overbaked, unstale, spot-ai, retro) archive each report to .ai/reviews/<YYYY-MM-DD>-<skill>[-<slug>].md by default; --no-archive opts out. Archiving is best-effort (never blocks the review) and warns — without editing anything — when .ai/ isn't gitignored in the target repo. When adding a new report-producing skill, adopt the same ## Archive block. Each archived report also carries a YAML provenance header (skill, skill-version, reviewed-repo + short SHA/dirty, cli, date) written to the archived file only, never to the report shown to the user — so a report re-read later can be placed against a revision of the skill's criteria. skill-version comes from a .version file deploy_skill() stamps into each deployed skill dir (the deployed dir is not a git checkout, so nothing else reaches that provenance at review time); it is gitignored as skills/*/.version because sync.sh pull would otherwise round-trip it back into skills/ as a tracked file, and it is absent — hence unknown — under a plugin install. Any value that fails to resolve becomes unknown rather than being dropped, and never blocks the archive. Gotcha (verified 2026-07-29): probe the ignore with the file path at repo root — git check-ignore -q "$(git rev-parse --show-toplevel)/.ai/reviews/…" — never the bare .ai directory. git check-ignore -q .ai false-negatives (exits "not ignored") for the directory-form .ai/ pattern this ecosystem uses whenever the .ai/ dir doesn't exist yet — e.g. the first archive in a freshly repo-init-scaffolded repo. All six review skills plus create-alert now use the correct form (migrated 2026-08-07). spot-ai goes further and skips archiving entirely when .ai/ isn't ignored or .ai/reviews is a symlink, because its reports can embed author-specific measured rates.

An archived report inherits its target's sensitivity. Reviewing a document that contains PII creates a second copy of that PII under .ai/reviews/ — and the review skills quote instances in context by design, so the copy is deliberate, not incidental (spot-ai quotes every finding's instance; figure-review quotes captions). Gitignoring .ai/ prevents publication, not accumulation: observed 2026-08-19, three archived reports had quietly collected six candidate names and a colleague's email address from two reviewed documents, none of it ever committed but all of it sitting in plain text. Two consequences. (1) After reviewing anything with third-party personal data, delete the archived report or accept that the directory now holds that data — nothing prunes it, and --no-archive is the cheap prevention at review time. (2) Don't trust a directory-level judgment about what .ai/reviews/ contains: the audit that found this had to grep every file for email addresses and names, and the report that was missed on a first pass was the one nobody expected to carry PII (a two-sentence daily note). The same reasoning extends to session scratchpads, which hold the source text a review was run against.

The probe and the mkdir must be anchored the same way. All six review skills were authored with a cwd-relative mkdir -p .ai/reviews against prose that said "under the repo root"; all six now anchor both lines to mkdir -p "$(git rev-parse --show-toplevel)/.ai/reviews" (migrated 2026-08-07). Invoked from a subdirectory, the old form scattered reports into <cwd>/.ai/reviews/ instead of collecting them at the repo root, so the archive was not one findable place.

Correction (measured 2026-08-07): this paragraph previously claimed a half-migration "lands the report in a tracked directory immediately after reporting 'yes, ignored.'" That is false for the pattern this ecosystem actually uses. A gitignore entry of .ai/ is unanchored, so it matches a directory named .ai at any depth — a subdirectory archive is still ignored. The tracking leak appears only under an anchored /.ai/ pattern, which neither this repo's .gitignore nor the one repo-init stamps (skills/repo-init/TEMPLATES.md:30) uses. Both forms were verified in a scratch repo. Keep the anchoring for findability and prose-code agreement; do not justify it with a tracking leak that only bites third-party repos using the anchored form. spot-ai is stricter than the other five by design — it skips archiving entirely when .ai/ isn't ignored or .ai/reviews is a symlink, because its reports can carry author-specific measured rates, and a third-party repo on the anchored form is exactly where that would leak. Note also that figure-review is the only review skill with a four-verdict vocabulary (pass / flag / by-design? / cant-assess) — the others have three, so don't assume the set is shared when editing one.

Review-skill design lessons — telling a chosen tradeoff from a defect (panel vs figure, publication-stage prior, reproductions, withdrawn findings that stay withdrawn, variability vs estimation uncertainty), and the blind-corpus calibration method for detector-style skills — live in docs/review-skill-design.md. Read it before changing any review skill's criteria.

Live-testing whether a skill activated: read the transcript, not the reply (learned 2026-10-01, briefly). A reply that looks compliant is no evidence: a short answer can be the model's default, and a tester can't see which path loaded the instructions. Grep the session's ~/.claude/projects/<mangled-cwd>/<id>.jsonl for the three distinguishable paths — <command-name>/<skill></command-name> (the CLI expanded a leading command), a Skill tool_use (the model invoked it), hook_additional_context (a hook injected it). This caught an entire test phase run without the hook registered, which every pasted reply had concealed. And for skills that shape output style (length, caveat count), one re-run per wording change is noise: a tightened rule loaded verbatim and was still not followed — test across repeated runs before editing wording again.

Worklog → Notion journal

The worklog skill captures a single work-log entry to three targets (all best-effort — none may block the caller): a local .ai/worklog-YYYY-MM-DD.jsonl mirror (source of truth), a redundant copy SSH-appended to ~/worklog/inbox/<date>.jsonl on the configured server (WORKLOG_SSH_TARGET), and a bullet appended to the current week's page in the Work Journal Notion space. It is model-invokable — it fires whenever the user asks to log/note/record something, and /handoff calls it after writing .ai/HANDOFF.md. An end-of-day Claude routine summarizes the day's raw entries into narrative sections on that weekly page.

  • Configuration: the server target (WORKLOG_SSH_TARGET) and the Notion Work Journal home page id (WORKLOG_NOTION_HOME) live in the env block of ~/.claude/settings.json (machine-local, not in this public repo). The skill distinguishes misconfiguration from transient failure: an unset var is surfaced loudly as a setup error, while a configured-but-unreachable target (server down, Notion MCP disconnected) stays best-effort and falls back to the local mirror. The specific IDs are recorded in machine-local memory, not here.
  • Weekly pages are sub-pages titled Week of YYYY-MM-DD (…) (Monday-anchored). Raw entries live under a ## 🗂️ Raw entries heading pinned as the last section — the append is an insert_content at page-end and depends on that section staying last.
  • Why OAuth, not a Notion token: the Climate Central workspace disables internal integration tokens, so the pipeline reuses the claude.ai Notion OAuth MCP (available in interactive sessions and cloud routines). This also means the summarization step must be a Claude cloud routine (which inherits the OAuth), not a server-side cron — a server cron can't reach Notion with OAuth and can't be reached from the cloud (private Tailscale network). The server hop is a redundant archive only.
  • Toggle→heading tradeoff: raw entries use a plain heading, not a collapsible toggle — reliably appending inside a toggle via the API is fragile (append tools add page-level blocks; nesting needs exact-match search-replace). The nightly routine clears the section, so it stays short regardless.

Boot hooks

HookScriptPurpose
SessionStart~/.claude/skills/tab-setup/scripts/hook-startup.shAuto-name and color-code each session on boot
SessionStartinline jq commandRe-pin model: opus + effortLevel: medium as the default on every boot (self-heals a mid-session /model//effort change)

hook-startup.sh is part of the tab-setup skill (deployed from dgilford/tab-setup). It is fully self-contained — no dependency on this repo. It generates a session name via Haiku API when ANTHROPIC_API_KEY is set, falling back to a deterministic wordlist hash otherwise; assigns a tab color; and prints [pickup] / [env] reminders to stderr. sync.sh push registers the hook in ~/.claude/settings.json automatically.

The key is optional, and the env block is the wrong place to put it. The wordlist fallback is the normal, intended state — docs/configuration.md and docs/tab-setup.md both document the key as optional, and no machine here has ever set it. Anything in settings.json's env block is exported into every Claude Code session, and ANTHROPIC_API_KEY takes precedence over subscription auth — so adding it there silently switches the machine from subscription to metered API billing, for the sake of a cosmetic session name. This is the same hazard guarded against elsewhere in this ecosystem: window-warmup's workflow warns "Do NOT set ANTHROPIC_API_KEY (it takes precedence and bills the API instead of the subscription)" and uses CLAUDE_CODE_OAUTH_TOKEN from claude setup-token; talim-server's heartbeat opens with a bare unset ANTHROPIC_API_KEY. If Haiku naming is ever actually wanted, inject a narrowly-scoped key into the hook's own process only — never the global env block.

Tab color assignments are persisted in ~/.claude/project-colors.json (cwd → {color, name, pid}), which is written by hook-startup.sh and never touched by the watcher. This drives color persistence through /clear (same PID) and claude -c (same cwd, dead PID not in use).

Model/effort pin hook. Interactive /model and /effort write their new value straight into ~/.claude/settings.json ("saved as your default for new sessions") — there is no per-session opt-in for model/effort (only fastModePerSessionOptIn exists, and it covers fast mode alone). To keep a durable default without giving that up, a second SessionStart hook re-pins the desired default on every boot via jq '.model="opus" | .effortLevel="medium"' (tmp-file + mv, with || rm -f cleanup so a failed jq never corrupts the file). A mid-session change still applies to the current session; the next boot heals it back — with a one-session-lag caveat (the session immediately after a change may briefly boot at the changed value before the hook fires; live-correction depends on the settings watcher, unverified). This hook is added manually per machine, not deployed by sync.sh — but it survives push, because sync.sh's SessionStart merge only appends hook-startup.sh when absent (any("hook-startup" in str(h) ...)) and never rewrites the existing matcher entry, and statusLine reconciliation touches only statusLine. To set it up on a new machine, add a second command to the SessionStart matcher "" hooks array: jq '.model="opus" | .effortLevel="medium"' ~/.claude/settings.json > ~/.claude/settings.json.tmp 2>/dev/null && mv ~/.claude/settings.json.tmp ~/.claude/settings.json || rm -f ~/.claude/settings.json.tmp.

Stacked-command warning hook. scripts/hook-warn-stacked-commands.sh is a UserPromptSubmit hook that warns when a submission begins with two or more slash commands — they all expand (docs/harness-behavior.md row 4), and that both bodies then run ungated is row 6, inferred. Warn-only and fails open; contracts and the reason set -f is load-bearing are in the script header. Three things worth knowing before writing any hook on this event: exit 0 stdout is injected as context Claude reads (so a hook that prints stray text is editing the prompt), exit 2 erases the user's prompt (this script never exits 2), and whether the event fires for mid-turn submissions is version-dependent — it did not at 2.1.220 but does at 2.1.286 (docs/harness-behavior.md row 12), so don't build a guard that depends on either answer. Like the model/effort pin hook, registration is manual per machine — add to the UserPromptSubmit array in ~/.claude/settings.json:

{ "matcher": "", "hooks": [{ "type": "command", "command": "bash ~/Projects/ai-science-toolkit/scripts/hook-warn-stacked-commands.sh" }] }

Briefly hook. scripts/hook-briefly.sh is a second UserPromptSubmit hook: when /briefly appears in a prompt that does not start with a slash command, it emits the briefly skill body (frontmatter stripped, read from the repo copy, falling back to the deployed one) as additionalContext, plus a user-visible systemMessage ("briefly: applied"), so the mid-prompt case is deterministic and costs no Skill round trip. It stays silent only when /briefly is among the leading slash commands (the CLI expands it there); a /briefly inside another command's arguments (/overbaked X /briefly) does fire, since the skill caps the reply's prose there too. Same contracts as the stacked-command hook (fail open, never exit 2); tests/smoke_hook_briefly.sh covers the match rules in CI. Known false positive: a pasted block that merely contains /briefly will trigger it. Manual registration, same UserPromptSubmit array:

{ "matcher": "", "hooks": [{ "type": "command", "command": "bash ~/ai-science-toolkit/scripts/hook-briefly.sh" }] }

Claude cannot edit hook commands in ~/.claude/settings.json — the permission classifier blocks it, via both Bash and the Edit tool (observed 2026-08-04; a hook command is code that runs on every prompt, so this is a deliberate guard, not a misconfiguration). Don't spend turns routing around it: either ask the user to paste the edit, or — usually better — instrument the hook script instead. The script is re-read on every invocation, so a script edit needs no restart and removes the "did the settings change take effect yet" confound that a settings.json edit introduces. Two further cautions when touching that file at all: its env block holds a live API key (ZOTERO_API_KEY; note it deliberately does not hold ANTHROPIC_API_KEY — see the billing hazard above), so never copy it wholesale into a scratch or temp directory for "backup" — do targeted string swaps with the original recorded instead; and remember the SessionStart pin hook rewrites the file on every boot, so an edit must survive a jq round-trip.

Syncing skills

Skills in skills/ are the source of truth. Use scripts/sync.sh — do not use cp -r directly (it creates nested directories when the destination already exists).

bash scripts/sync.sh push             # deploy skills/ → ~/.claude/skills/; agents/ → ~/.claude/agents/; register hook-startup.sh
bash scripts/sync.sh push <name>...   # deploy only the named skills/agents (piecemeal install)
bash scripts/sync.sh pull             # pull ~/.claude/skills/ → skills/; ~/.claude/agents/ → agents/

Named push auto-detects each name as a skill (skills/<name>/) or agent (agents/<name>.md) and transitively auto-includes co-deployed runtime dependencies from the skill_deps() map in scripts/sync.sh — that map is the single source of truth for skill→skill wiring (this file deliberately doesn't duplicate the list; it drifted when it did). "Dependency" spans hard (the caller no-ops without it) and graceful-degrade (e.g. repo-init asks its intake questions inline if grilling is absent) — co-deploy both kinds. Update skill_deps() when wiring changes (lint_skill_refs() catches in-repo renames but not this map). A named push skips settings registration (hook/statusline) unless tab-setup is among the resolved names, and skips the external-repo refresh likewise. Exclusion interplay: an explicitly named skill overrides its machine-local exclusion; a dependency-added one still honors it.

After pull, review git diff skills/ agents/ — pull brings in all globally installed skills and agents, including any not yet tracked in this repo.

Always edit skills/ (the repo copy), never ~/.claude/skills/ directly. push overwrites the installed copy from the repo — edits to the installed copy are silently lost on the next push.

A merge does not deploy — so after merging a branch that touched skills/ or agents/, check for drift and ask the user whether to deploy. Never deploy on your own initiative. push sits outside every git path, so a merge leaves the repo correct and the thing that actually runs stale, with nothing to announce it: the drifted skill keeps working, just at its old behavior. Observed 2026-08-04 — a merged skills/handoff/SKILL.md change went undeployed and the very next /handoff run executed the old body (it happened to lose only a claude --version preamble, and only because the version was already in context did the omission cost nothing). To check: git diff <merge-base>..HEAD --stat -- skills/ agents/ names what changed, and comparing a repo SKILL.md against its ~/.claude/skills/<name>/ counterpart confirms live drift. Then surface the affected names and let the user choose. On an explicit go-ahead, run the named push yourself; the auto-mode permission classifier has blocked it once (2026-10-30) and allowed it once (2026-11-06, CLI 2.1.319), so the outcome is not predictable (register row 20). A block is visible, so try once, and if it is blocked hand the user the command as a single-line ! bash scripts/sync.sh push <names> — a wrapped paste splits into two shell commands. The reason this is a prompt and not an automatic step: deploying is a change to the user's live environment, a full push additionally rewrites settings registration and refreshes the external tab-setup fork (a named push <skill> does neither unless tab-setup resolves), and a deploy mid-session changes how subsequent invocations behave in a session already underway. Note there is no drift subcommand — sync.sh orphans reports deployed-without-repo-counterpart, not content drift — so the comparison is manual until someone adds one.

push never prunes. deploy_skill() is mkdir -p + cp -r "$SKILLS_SRC/$1/." "$SKILLS_DEST/$1/" — an overlay with no delete pass — and deploy_agent() is a plain cp. (That trailing-/. form is what avoids the nested-directory trap named above; it does not prune.) So three things survive deletion from the repo: a renamed or removed skill dir, a renamed or removed agent .md, and a file deleted from inside a skill that still deploys. All keep resolving from ~/.claude/ indefinitely. After any rename (git mv) or deletion (git rm -r), delete the deployed copy by hand — and do it before the next pull, because pull copies every deployed dir back into skills/, re-adding the old name to the repo. bash scripts/sync.sh orphans lists deployed skills and agents with no repo counterpart (report-only; it never deletes, and it will also list legitimately third-party skills installed outside this repo). Why this matters beyond tidiness: resume was renamed to pickup precisely because a local skill shadows Claude Code's built-in /resume past-session picker, and a lingering deployed resume/ would silently reinstate that shadowing.

Running push on Windows (Git Bash). The script is Unix-authored and has four rough edges here; all four were hit on a first run, and the first two are unfixed bugs in sync.sh itself — delete these notes when they're fixed. (1) Most python3 heredocs call open() with no encoding=, so Windows' cp1252 default raises UnicodeDecodeError on the non-ASCII already present in skill files. lint_skill_refs() now passes encoding="utf-8" (it had to — it reads the widest file set in the repo), but lint_frontmatter(), the settings-merge heredocs, and scripts/gen-docs.sh still don't. Prefix PYTHONUTF8=1 until they do — worth fixing in-repo rather than per-machine, since the repo is public and plugin-distributed. (2) The tab-setup review gate's read -r reply runs under set -euo pipefail, so on non-TTY stdin the EOF return aborts the script silently — it never reaches the intended ✗ declined — rolling back path, deploying nothing and rolling back nothing. That's what a !-shell or piped invocation gets; read -r reply || reply=n would make it degrade to "No" as designed, and SYNC_EXTERNAL_ACCEPT=1 is the deliberate non-interactive escape hatch. (3) Missing PyYAML degrades inconsistently — lint_frontmatter() skips with a warning, but tests/smoke_repo_init.py hard-fails on every yaml block, so the push dies with a confusing template error instead of one clear "install PyYAML". (4) On Windows python3 usually resolves to the Windows Store stub, which prints an install prompt and exits nonzero rather than failing cleanly; a python3.exe copy of python.exe in the real install dir fixes it if that dir already precedes WindowsApps on PATH. Note also that Git for Windows puts bash.exe in Git\bin, which the installer does not add to PATH (only Git\cmd, which has git.exe alone).

Machine-local push exclusions: to keep a skill out of push on one machine without affecting others, list its dir name (one per line, # comments allowed) in ~/.claude/sync-skills-exclude. push skips each match (printing ⤫ <name> (excluded…)) via is_excluded(). The file is machine-local and untracked, so the choice never propagates through the repo; the mechanism itself lives in scripts/sync.sh. This is a deploy gate only — nothing here deletes (see "push never prunes" above), and a skill can still be disabled other ways (e.g. on the deployment where worklog is excluded, it is also inert without its WORKLOG_* creds — but that exclusion is that machine's local choice, not a repo-wide fact).

Keeping a not-yet-public skill in this public repo: leave its directory untracked and list it in .git/info/exclude (machine-local — a .gitignore entry would itself publish the name). Two couplings make this a fragile state, learned building one such skill: (1) gen-docs.sh write regenerates the catalog tables from the filesystem, so CLAUDE.md/README acquire rows referencing the excluded skill — those files become uncommittable until the skill ships (a checkout without the skill fails both gen-docs.sh check and lint_skill_refs in CI), which forces go-public to be one atomic commit: skill + tables + pathfinder row + axis-list entry together, then delete the exclude line. (2) Untracked + excluded means git clean -fdx deletes the only repo copy — keep a backup outside the repo while the state persists.

External skills (tab-setup) are a special case: push pulls from github.com/dgilford/tab-setup (a fork of JeraldHuff/tab-setup) into tab-setup/ before copying into skills/tab-setup/, so edits to skills/tab-setup/scripts/ and skills/tab-setup/vscode-extension/ are overwritten by this pull — edit tab-setup/scripts/, commit/push to the fork, then sync.sh push. The refresh is narrower than it looks, and the difference is load-bearing: sync_external_skills() copies only those two subdirectories (scripts/sync.sh:185-194) and never SKILL.md, which is why the local catalog: block that skills/tab-setup/SKILL.md carries — claude_md: false, repo_url, provenance — survives a push and gen-docs.sh check keeps passing. The fork's own SKILL.md has no catalog: block; if the refresh ever widened to the whole tree, every push would silently drop the tab-setup row out of README's table. The push now shows a diff and asks before deploying changed upstream code (it becomes a SessionStart hook). Fork-maintenance procedures (upstream sync, /tab-setup update, caveats): docs/tab-setup-maintenance.md.

The claude-tab VS Code extension is deliberately duplicated — author in the fork (tab-setup/vscode-extension/, what deploys), then mirror into the tracked vscode-extension/ (what CI tests); lint_vscode_extension() fails the push on drift. Its silent failure mode, the stale-.repo-path trap, and three extension.js editing rules — including a \r-injection guard that has no regression test, so don't remove it as dead code — are in docs/tab-setup-maintenance.md.

Plugin distribution

The repo is also installable as a Claude Code plugin via .claude-plugin/marketplace.json + .claude-plugin/plugin.json at the repo root. One-line adoption:

/plugin marketplace add dgilford/ai-science-toolkit
/plugin install ai-science-toolkit@ai-science-toolkit
  • Single bundle plugin, source: "." (repo root). plugin.json carries metadata only — no skills/agents component fields — so Claude Code auto-discovers skills/*/SKILL.md and agents/*.md. This ships the same skills and agents as a full sync.sh push (everything, including tab-setup; unlike push, it isn't narrowed by a machine's local exclude-list and doesn't register the boot hook or refresh the tab-setup fork), so no manifest edit is needed when adding a skill or agent. Note the default skills/ scan always runs and the skills field only adds to it — there is no way to exclude a skill (e.g. tab-setup) short of relocating it out of skills/.
  • tab-setup under the plugin path is inert, not broken: its SessionStart auto-naming hook is registered by sync.sh into ~/.claude/settings.json, not by a hooks/hooks.json, so a plugin install ships the /tab-setup command but never activates the boot hook. Users who want the hook must use sync.sh push.
  • ${CLAUDE_PLUGIN_ROOT} gotcha (companion files): a plugin installs under ~/.claude/plugins/cache/…, not ~/.claude/skills/, so any skill that loads a companion file must not hardcode ~/.claude/skills/<name>/. Use ${CLAUDE_PLUGIN_ROOT:+$CLAUDE_PLUGIN_ROOT/skills/<name>} with a $HOME/.claude/skills/<name> fallback so the same line resolves under both plugin install (var set → plugin root) and sync.sh deploy (var unset → home). Empirically confirmed (2026-07-20): Claude Code does export CLAUDE_PLUGIN_ROOT into a skill's ```! preamble under a plugin install (not just for hooks/commands) — verified by loading /figure-review from a real /plugin install and seeing both companion files resolve — so the plugin-root branch actually fires; the pattern is not merely theoretical. figure-review's COLORBLIND.md/CC-STYLE.md loads are the reference example; create-alert's HARDENING.md (an injection-hardening block baked verbatim into every cloud-routine prompt it generates — the reusable pattern for any skill that authors an unattended agent holding a connector + shell) uses the same load line; lit-review's relative markdown link to REFERENCE.md needs no such handling (resolved from the SKILL.md's own dir). Apply this rule to any new companion-file skill.
  • Versioning: two of the repo's four version fields live here — the marketplace manifest's top-level version and plugin.json (the plugin entry in marketplace.json carries no metadata beyond name/source/category/description; everything else is inherited from plugin.json, the authoritative source). Bump both on a tagged release so /plugin marketplace update surfaces the change. The other two are CITATION.cff and .zenodo.json — see the full list under Releases & citation. Nothing lints any of them against git tags.

Cloud routines

Warmup infrastructure (the two-tier claude -p window-anchoring system and its warmup health check routine) lives in the private infra repo, not here.

A weekly cloud routine, disable-model-invocation bug watch (ID in .ai/routines.md), polls anthropics/claude-code#22345 + the CHANGELOG and alerts when the token-reclaim bug (see Skill file format) is fixed; retire it then. Caveat: #22345 is titled as a plugin-skills issue — a weak proxy in both directions — so on any FIXED alert, verify token reclaim empirically before acting.

Manage routines at https://claude.ai/code/routines — IDs and creation notes in .ai/routines.md (gitignored, machine-local).

Routine conventions & constraints (apply to any routine authored by create-alert or edited via the schedule skill / RemoteTrigger API):

  • Standard alert output follows create-alert's house format: lead each Slack message with a status glyph — 🔵 the watched-for thing happened · 🟢 ran clean, nothing to report · ⚠️ couldn't run (source/auth failure) — in Slack mrkdwn (single-asterisk *bold*, :emoji:, •, ·; never **/#), with fail-loud delivery (a 🔵 that fails to send is a hard error, never a silent success). The disable-model-invocation bug watch and the (infra-repo) warmup health check routines were standardized to this scheme.
  • Cloud routines have no secret store. Per Claude Code docs, "a dedicated secrets store is not yet available" — env vars set on a cloud environment are plaintext-readable to anyone using that environment. Use only for a single-user private environment, with a minimally-scoped, rotatable token.
  • Never inline a secret in a routine prompt. The RemoteTrigger create/update classifier blocks any body containing an inline credential (e.g. a github_pat_…), so a credentialed routine must reference it via an env var (e.g. Bearer $SOME_PAT) — this is both the security practice and a hard requirement for programmatic updates to go through.
  • Setting a routine's env vars is a UI action (no API, no standalone settings URL): claude.ai/code → open the routine's edit form → the cloud-icon environment selector below Instructions → hover the environment → gear → Environment variables (.env format).

Releases & citation

Four version slots (CITATION.cff + date-released, .zenodo.json, .claude-plugin/plugin.json, .claude-plugin/marketplace.json) and two DOI slots must change together on a release, and nothing lints them. Procedure, the DOI convention (don't swap the version and concept DOIs), and the Zenodo lookup: docs/releasing.md.

Commits

A tracked pre-commit hook (.githooks/pre-commit) runs sync.sh lint + gen-docs.sh check — enable once per clone with git config core.hooksPath .githooks. Don't gate a commit on piped lint output (lint | grep -c ✓ && git commit): the pipeline's exit status is grep's, so a failing lint still commits.

main is branch-protected (PR + 3 required status checks), but the owner's account can bypass it — a direct git push origin main succeeds with only a remote: Bypassed rule violations notice, so CI never gates the change. Merge feature branches via gh pr create, not a local merge + push, unless the user says otherwise (bypassed by the v1.1.0/v1.2.0 release commits and the disclose merge; whether release commits are a sanctioned exception is open — see docs/releasing.md).

Global GPG signing is disabled (commit.gpgsign is unset in ~/.gitconfig), so commits are unsigned by default and can be made in-session without a passphrase prompt. The signing key (user.signingkey) is still configured — opt into a signed commit per-commit with git commit -S. To restore signing-by-default: git config --global commit.gpgsign true (note: pinentry's GUI times out in this environment, so signed commits would then need an external terminal with the key already unlocked).

origin/HEAD may be unset in a clone, which breaks any skill whose preamble diffs against it — /security-review fails with "ambiguous argument 'origin/HEAD...'" rather than degrading. Fix once per clone: git remote set-head origin --auto. Also note those preambles use a three-dot commit range, so they see committed work only; to review an uncommitted working tree, diff it explicitly rather than assuming the skill picked it up.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.