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.
This skill should be used when executing an already-scanned Canvas plan after the user has approved it.
$ npx skills add X-isdoingreat/canvas-pilot --skill canvas-execute -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install X-isdoingreat/canvas-pilot canvas-execute --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/X-isdoingreat/canvas-pilot.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/canvas-execute .claude/skills/canvas-execute && 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 "canvas-execute" agent skill from https://github.com/X-isdoingreat/canvas-pilot/tree/main/.claude/skills/canvas-execute into .claude/skills/canvas-execute/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "canvas-execute", 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/X-isdoingreat/canvas-pilot/tree/main/.claude/skills/canvas-executeType 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 X-isdoingreat/canvas-pilot --skill canvas-execute -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install X-isdoingreat/canvas-pilot canvas-execute --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/X-isdoingreat/canvas-pilot.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/canvas-execute .agents/skills/canvas-execute && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "canvas-execute" agent skill from https://github.com/X-isdoingreat/canvas-pilot/tree/main/.claude/skills/canvas-execute into .agents/skills/canvas-execute/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "canvas-execute", 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 X-isdoingreat/canvas-pilot --skill canvas-execute -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install X-isdoingreat/canvas-pilot canvas-execute --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/X-isdoingreat/canvas-pilot.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/canvas-execute .cursor/skills/canvas-execute && 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 "canvas-execute" agent skill from https://github.com/X-isdoingreat/canvas-pilot/tree/main/.claude/skills/canvas-execute into .cursor/skills/canvas-execute/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "canvas-execute", 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/X-isdoingreat/canvas-pilot.git --path .claude/skills/canvas-execute--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 X-isdoingreat/canvas-pilot --skill canvas-execute -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install X-isdoingreat/canvas-pilot canvas-execute --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/X-isdoingreat/canvas-pilot.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/canvas-execute .gemini/skills/canvas-execute && 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 "canvas-execute" agent skill from https://github.com/X-isdoingreat/canvas-pilot/tree/main/.claude/skills/canvas-execute into .gemini/skills/canvas-execute/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "canvas-execute", 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 X-isdoingreat/canvas-pilot canvas-executeInstalls 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 X-isdoingreat/canvas-pilot --skill canvas-execute -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/X-isdoingreat/canvas-pilot.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/canvas-execute .github/skills/canvas-execute && 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 "canvas-execute" agent skill from https://github.com/X-isdoingreat/canvas-pilot/tree/main/.claude/skills/canvas-execute into .github/skills/canvas-execute/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "canvas-execute", 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 X-isdoingreat/canvas-pilot --skill canvas-execute -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install X-isdoingreat/canvas-pilot canvas-execute --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/X-isdoingreat/canvas-pilot.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/canvas-execute .opencode/skills/canvas-execute && 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 "canvas-execute" agent skill from https://github.com/X-isdoingreat/canvas-pilot/tree/main/.claude/skills/canvas-execute into .opencode/skills/canvas-execute/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "canvas-execute", 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.
canvas-executeThis skill should be used when executing an already-scanned Canvas plan after the user has approved it.
Canvas Execute is an agent skill from X-isdoingreat/canvas-pilot. This skill should be used when executing an already-scanned Canvas plan after the user has approved it. Invoked after canvas-scan wrote runs/<today/plan.json and the user replied with approval like "批准全部", "approve all", "做 1, 3, 5", "只做 urgent", "第 N 项 defer", "cancel". Parses the approval, updates plan.json with per-item decisions, dispatches approved items to course-specific sub-skills (canvas-ics33 / canvas-reading-annotation / canvas-essay / canvas-inside / canvas-zybooks / canvas-generic / canvas-skip)…
Its SKILL.md is about 6.6k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It works with Bash. The repository describes itself as: Local-first Canvas LMS AI agent that learns each course's recurring assignment workflow and reuses it through scan - approval - execute with student review. The licence is AGPL-3.0.
9 steps, taken from the step headings in SKILL.md.
Read from SKILL.md and the folder at commit 6b79d5b. It shows what the files ask for, not the result of running them.
Pre-approves these tools, so the agent can use them without asking each time:
BashReadWriteEditGlobGrepWebFetchSkillTodoWriteFrom allowed-tools in the SKILL.md frontmatter.
No scripts in the folder and no shell commands in SKILL.md (its code samples are python, markdown, bash and json).
From the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From 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.
Canvas Execute loads about 6.6k tokens when it runs. Until then it costs about 164 tokens; SKILL.md has 2,578 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 noted patterns worth knowing about, such as sudo or a known installer.
thon helpers under `src/`, config under `.env` and `courses.yaml`.allowed-tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, Skill, TodoWriteAutomated 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 X-isdoingreat/canvas-pilot at commit 6b79d5b, republished under its AGPL-3.0 licence (© X-isdoingreat). 2,578 words, ~6,572 tokens.
.claude/skills/canvas-execute/SKILL.md (or your agent's skills folder).Dispatcher for the Canvas auto-homework system. This skill is the execute half of the scan/execute split — it runs only after canvas-scan has produced a plan and the user has explicitly approved some subset of it.
canvas-scan wrote runs/<today>/plan.json with every pending item at user_decision: null.This skill's job: apply the approval to plan.json, dispatch the approved items, record the non-approved ones as deferred, finalize.
If plan.json doesn't exist or is >24h old → STOP and tell the user to run /canvas-scan first. Do not invent a plan, do not dispatch anything, do not guess.
The highest spec is canvas_scan.md §7 (batch execution after approval). Read it if unsure.
All 7 hooks apply during execute:
| # | Hook | Effect |
|---|---|---|
| 1 | SessionStart inject-context.py | Injects pending list at turn 0. Informational. |
| 2 | PostToolUse(Write/Edit on result.json) check-result-schema.py | Every result.json you write must have valid status (draft_ready/submitted/skipped/error). Bad schema → exit 2. |
| 3 | PostToolUse(Bash) check-bash-output.py | Validates pytest / coverage / git output when sub-skills run them. |
| 4 | PreToolUse(Bash) check-presubmit-audit.py | Blocks cv.submit_files / upload_submission_file / submit_quiz unless <work>/verification.log exists and is all PASS. |
| 5 | PostToolUse(Write/Edit on result.json) check-spec-grounding.py | For draft_ready/submitted, if spec.md mentions external refs but references/ is empty → exit 2. |
| 6 | PostToolUse(Write/Edit on result.json) check-identifier-grounding.py | For .py/.pdf drafts, every suspicious Python identifier must be grounded in spec/refs/builtins. |
| 7 | Stop hook check-router-complete.py | Gated by runs/<today>/.scan_in_progress marker. While the marker exists, every item in assignments.json must have a result.json before Stop passes. |
Critical: hook 7 is the one that keeps execute honest. Without it, a session could dispatch 3 of 5 approved items and then drift off. With it, you cannot stop until every assignment in assignments.json has a result.json — either written by a sub-skill (for approved+executed) or written by this skill as status: skipped with deferred_to_next_run: true (for non-approved or skipped-at-approval-time).
The working directory is the project root (the folder you cloned this repo into; Claude Code sets cwd automatically on session start). Python helpers under src/, config under .env and courses.yaml.
TODAY=$(date +%Y-%m-%d)
test -f "runs/$TODAY/plan.json" || { echo "NO_PLAN"; exit 1; }
test -f "runs/$TODAY/assignments.json" || { echo "NO_ASSIGNMENTS"; exit 1; }If either is missing: tell the user "No plan.json for today. Run /canvas-scan first to generate a plan, review it, then come back." Stop. Do NOT run scan inline — that's the router's job and would defeat the approval gate.
Then check plan.json freshness:
import json, datetime as dt
plan = json.loads(open(f"runs/{today}/plan.json", encoding="utf-8").read())
expires = dt.datetime.fromisoformat(plan["expires_at"])
now = dt.datetime.now(expires.tzinfo)
if now > expires:
print("PLAN_EXPIRED")
# tell user: "Plan is stale (>24h). Re-run /canvas-scan before executing."If expired: STOP. Tell user to rerun /canvas-scan.
Before activating your own marker, check for an orphan marker from a past session:
if [ -f "runs/<past-date>/.scan_in_progress" ]; then ...Glob runs/*/.scan_in_progress. For each match whose date is not today:
assignments.jsonresult.json, write {status: "skipped", notes: "session crashed before this item was processed", deferred_to_next_run: true} so the next /canvas-scan re-proposes itThis keeps Stop hook from being permanently wedged by a past crash.
The marker file's contents are the creating session's id, not just a touch. The Stop hook (check-router-complete.py) reads the marker contents and compares to its own event's session_id — if they don't match, the hook treats this as "another session's marker, not mine" and passes through. This is what lets a debug-only session (in the same project, never called canvas-execute) stop cleanly while another session is mid-execute.
CC must extract the current session id from the env / hook event / transcript path and write it into the marker:
import os, uuid
from pathlib import Path
import datetime as dt
today = dt.date.today().isoformat()
marker = Path(f"runs/{today}/.scan_in_progress")
marker.parent.mkdir(parents=True, exist_ok=True)
# Determine session_id. CC has its own session_id available in the runtime
# environment — check CLAUDE_SESSION_ID env var first, fall back to extracting
# from the transcript filename ($CLAUDE_TRANSCRIPT_PATH or the conversation
# transcript path), and as a last resort generate a uuid (this still works:
# the hook only requires "marker contents == event.session_id"; if execute
# stamped a uuid that doesn't match the runtime session id, the hook treats
# the session as not-the-owner and passes through, which is safe — you just
# lose the gate for this run, but no data loss).
session_id = (
os.environ.get("CLAUDE_SESSION_ID")
or os.environ.get("CC_SESSION_ID")
or str(uuid.uuid4()) # safe fallback
)
marker.write_text(session_id, encoding="utf-8")Practical note for CC: at execute time, read the most reliable session id source you have — typically the transcript path filename matches the session uuid. If you can't determine the current session id deterministically, use uuid4 as fallback; this still implements "this session vs other sessions" correctly within this single execute run.
This arms the Stop hook. Every assignment in runs/$TODAY/assignments.json must now have a matching result.json before this session can stop — for sessions whose session_id matches the marker contents. Other sessions in the same project pass through.
Read the user's approval spec. It was either:
"approve all", "只做 urgent", "做 1, 3, 5")Recognize these patterns (Chinese + English):
| User says | Interpretation |
|---|---|
| 批准全部 / 全部批准 / approve all / all / 全部做 | every item → approve |
| 只做 urgent / urgent only / 只做紧急 | items with bucket: urgent → approve, rest → defer |
| 做 1, 3, 5 / 做 1 3 5 / approve 1 3 5 | listed indices → approve, rest → defer |
| 做 1-4 / 1 到 4 | index range → approve, rest → defer |
| 第 N 项 用 canvas-X / swap N to canvas-X | item N gets swap:canvas-X, still approved but with different skill |
| 第 N 项 defer / 跳过 N / skip N | item N → defer |
| cancel / 取消 / 全部取消 | every item → defer (nothing dispatched, all written as skipped+deferred) |
| 未明示 (e.g. user says "做 1, 3" silent on 2,4,5) | 2, 4, 5 → defer (safer than auto-executing) |
Ambiguous or unparseable input → STOP and ask the user once more. Do not guess. Example ambiguous input: "做前面几个" (how many is "前面"?). Answer: "请明确一下 —— 做第 1、2、3 项吗?" and wait.
Update plan.json atomically:
import json, os
plan_path = Path(f"runs/{today}/plan.json")
plan = json.loads(plan_path.read_text(encoding="utf-8"))
for item in plan["items"]:
item["user_decision"] = determine_decision(item) # "approve" | "defer" | "swap:canvas-X"
tmp = plan_path.with_suffix(".json.tmp")
tmp.write_text(json.dumps(plan, indent=2, ensure_ascii=False), encoding="utf-8")
os.replace(tmp, plan_path)For each item in plan.json where user_decision == "approve" or starts with "swap:", process sequentially (plan.json items are already sorted by bucket priority then by hours_left, so earliest-due-first).
For each item:
Determine which sub-skill to invoke:
user_decision == "swap:canvas-X" → canvas-X (user explicit override wins)
user_decision == "approve" → resolve the Skill name from proposed_skill. proposed_skill may be a short courses.yaml route value OR already a canvas-* Skill name; map it to the Skill to invoke:
route value (proposed_skill) | Skill to invoke |
|---|---|
quiz | canvas-inside |
code_py | canvas-ics33 |
ac_english | canvas-reading-annotation (or canvas-essay via the sub-router below) |
zybooks | canvas-zybooks |
mixed_unsupported | canvas-skip |
A proposed_skill already in canvas-* form (e.g. the LDB filter rewrites quiz items to canvas-skip) maps to itself.
Writing-course sub-routing: if the resolved skill is canvas-reading-annotation, run the deterministic router to decide between the short-form handler (canvas-reading-annotation, unchanged) and the long-essay handler (canvas-essay). This is a pure-Python utility — 0 LLM calls, regex + numeric comparison + Canvas API field queries.
if proposed_skill == "canvas-reading-annotation":
import re, yaml
from pathlib import Path
from src.ac_eng_router import route_ac_eng_assignment
from src import canvas_client as cv
# Pull the overlay's persona_trigger_patterns / persona_skip_patterns yaml block
overlay_path = Path("_private/canvas-essay-app.md")
overlay_config = {}
if overlay_path.exists():
text = overlay_path.read_text(encoding="utf-8")
m = re.search(r"```yaml\s*\n(persona_trigger_patterns:.*?)\n```",
text, re.DOTALL)
if m:
overlay_config = yaml.safe_load(m.group(1)) or {}
# Look up the full assignment dict (router needs name, description,
# points_possible, submission_types, attached_pdf_texts).
full = next(
(a for a in assignments_json if str(a.get("id")) == str(item["assignment_id"])),
None,
)
if full is None:
full = cv.get_assignment(item["course_id"], item["assignment_id"])
sub = route_ac_eng_assignment(full, overlay_config, plan_item=item)
if sub == "essay":
proposed_skill = "canvas-essay"
# else keep canvas-reading-annotation for short-form pathIf _private/canvas-essay-app.md does not exist, overlay_config stays empty — the router still works (skips Layer 2, relies on Layers 3-6). The router never raises; on any malformed regex in the overlay it logs and falls through.
TodoWrite: add the assignment as a todo, mark in_progress.
Invoke via the Skill tool. Pass a brief context line:
"Work on
<assignment_name>(course<course_name>). Work dir:runs/<today>/<work_dir>. Seeassignments.jsonandplan.jsonfor full item details."
Sub-skill runs → writes its own result.json → returns.
Read the result.json. Mark todo completed (or keep in_progress with error note if status: error).
Update runs/_processed.json ledger (atomic write via .tmp + os.replace):
ledger[f"{course_id}:{assignment_id}"] = {
"status": sub_result["status"],
"course_name": item["course_name"],
"assignment_name": item["assignment_name"],
"due_at": item["due_at"],
"completed_at": dt.datetime.now(dt.timezone.utc).isoformat(),
"draft_path": sub_result.get("draft_path"),
"notes": sub_result.get("notes"),
"deferred_to_next_run": False,
}Continue to the next approved item.
Run items sequentially (not in parallel) — Canvas writes can race, and sequential logs are easier to debug.
If the Skill tool reports the target sub-skill is not discoverable, STOP and print a clear error ("sub-skill canvas-X not found; CC was likely launched from the wrong directory"). Do NOT try to do the work inline.
Claude judges its own context budget. If roughly less than ~15 turns of capacity remain AND there are still approved items left to dispatch, break out of the loop instead of trying to squeeze in another heavy item. Half-finished work is worse than deferred work.
To pause cleanly (Stop hook requires every assignments.json item to have a result.json):
For each remaining undispatched approved item, write a placeholder result.json:
{
"assignment_id": ...,
"course_id": ...,
"status": "skipped",
"notes": "paused mid-run — awaiting user decision (continue or defer)",
"deferred_to_next_run": true
}Also update _processed.json ledger with deferred_to_next_run: true. This satisfies the Stop hook.
Print a clear status to the user, in this shape:
做完了:
- #1 Quiz on Section 5 — draft_ready (runs/.../quiz_s5.json)
- #2 Quiz on Section 6 — draft_ready (runs/.../quiz_s6.json)
还没做(session 快满了):
- #3 Set 3 Problem 1
- #4 Set 3 Problem 2
- #7 Tue Wk5 HW Scan
要继续做吗?
- 回 "继续" → 我接着跑 #3,撑到哪算哪
- 回 "defer" / "算了" / 不回 → 剩下的保持 deferred,下次 /canvas-scan 自动重现End your turn. Do NOT proceed to §6 finalize yet — wait for the user's next reply.
On the user's next turn:
result.json with a real one. Repeat §5 pause-check as you go.result.json files stay as the final state (they're already correctly marked deferred_to_next_run: true).Rule of thumb on when to pause: if the next item's proposed_skill is canvas-ics33 (heavy, ~20 turns) or canvas-generic (heavy, ~25-30 turns — investigation + 3 sub-agent reviews) and your context window is >60% consumed → pause. If it's canvas-inside or canvas-skip (light, ~10 or ~1 turns) → probably fine to attempt one more. Trust your judgment; erring toward pausing is safer than runaway.
Reach this section via one of two paths:
Steps:
Write deferred result.json for items not yet written. These are items where:
user_decision == "defer" (explicitly declined at approval time) — if their result.json doesn't already existuser_decision == null (user silent on this index at approval time) — same{status: "skipped", notes: "user declined at plan review" / "not approved this run", deferred_to_next_run: true}Update runs/_processed.json ledger for every item processed this run (dispatched or deferred).
Write runs/<today>/REPORT.md (see §7 for urgent banner + §8 for layout).
Sync final_drafts/ folder for any draft_ready/submitted items (see §9).
Remove marker: rm "runs/$TODAY/.scan_in_progress". Pair with §2's touch. If this step is skipped, the next session in this directory is gated.
Compute urgency for EVERY item in assignments.json (whether approved, deferred, or already done):
import json, datetime as dt
from pathlib import Path
now = dt.datetime.now(dt.timezone.utc)
ledger = json.loads(Path("runs/_processed.json").read_text(encoding="utf-8"))
todays = json.loads(Path(f"runs/{today}/assignments.json").read_text(encoding="utf-8"))
urgent = []
for item in todays:
key = f"{item['course_id']}:{item['assignment_id']}"
led = ledger.get(key, {})
# Canvas is source of truth for "is it submitted"
try:
sub = cv.get_submission(item["course_id"], item["assignment_id"])
live_state = sub.get("workflow_state")
except Exception:
live_state = "?"
if live_state in ("submitted", "graded"):
continue # done, skip from urgent banner
due = dt.datetime.fromisoformat(item["due_at"].replace("Z", "+00:00"))
hours_left = (due - now).total_seconds() / 3600
if hours_left <= 24:
# Prefer friendly name if the overlay set one (plan.json carried it through from scan)
course_display = item.get("course_friendly_name") or item["course_name"]
urgent.append({
"course": course_display[:25],
"name": item["name"][:50],
"hours_left": round(hours_left, 1),
"state": live_state if hours_left > 0 else "OVERDUE",
"ledger_state": led.get("status"),
"draft": led.get("draft_path"),
"skill": item["skill"],
})The same friendly-name preference applies to the error block (§7.5), the success summary, and the final_drafts/ folder structure — wherever a course label is rendered for the student, course_friendly_name (when present in plan.json/result.json) takes precedence over course_name. The framework name (e.g. canvas-ics33) is the dispatch key and is unaffected.
Format banner:
# 🔥 URGENT — N item(s) due within 24h, not submitted
- [canvas-ics33] <Code Course> Spring 2026 | Set 3 Problem 1 | due in 14h | state=unsubmitted | draft: runs/.../set3p1.py
- [canvas-reading-annotation] <Writing Course> S26 | Tue Wk5 HW Scan | OVERDUE 3h ago | state=OVERDUE | ledger=draft_ready
Upload the draft, mark 'skip on purpose', or handle it. This banner reappears every run until resolved.
---If no urgent items:
# ✅ No urgent items in next 24h
---The banner is ALWAYS the first block of REPORT.md. The CEO opens the file and sees status immediately.
For every item where this run wrote result.json with status: "error", REPORT.md gets a debug-help block right below the urgent banner. The student sees a concrete next-step list, not just notes: "...".
Iterate runs/<today>/*/result.json, collect the ones with status == "error", and for each render:
## ⚠️ Errors this run — N item(s) need fixing
### {assignment_name} ({course_name})
- skill: `canvas-{name}` → `.claude/skills/canvas-{name}/SKILL.md`
- error notes: {result.json notes verbatim}
**Debug checklist** (open the SKILL.md and tick through):
- [ ] `<!-- UNFILLED_SKELETON v1 -->` sentinel still present? Remove it after you fill the 4 TODOs.
- [ ] Frontmatter `name:` matches the directory name (`canvas-{name}`)?
- [ ] Frontmatter `allowed-tools:` includes everything the skill uses (Bash / Read / Write / Edit / WebFetch)?
- [ ] §1 TODO answered (where does the real spec live)?
- [ ] §2 TODO answered (how does this skill produce a draft)?
- [ ] §3 TODO answered (how do you verify the draft before submitting)?
- [ ] §4 result.json — does the skill actually write `runs/<today>/<dir>/result.json`?
- [ ] If `notes` mentions a specific file path or API call: does that path exist? does the API call work standalone (`python -c "from src import canvas_client; ..."`)?
After fixing, re-run `/canvas-scan` — the assignment will reappear in the plan (deferred items re-enter on next scan).
---If multiple errors, list them all under one ## ⚠️ Errors this run heading. If zero errors, skip this section entirely (don't render an empty heading).
This block exists because raw error notes is hostile to a first-time student — they get "X failed" and don't know whether the problem is their SKILL.md, the framework, or Canvas. The checklist gives them a 30-second sanity scan + a specific re-run command.
Same principle as canvas-scan §5b: data without recommendation is a bug. After all status sections, REPORT.md ends with a ## Next step block giving ONE concrete suggested action.
The suggestion adapts to what actually happened this run:
/canvas-scan 重跑。" / English equivalent.<list assignment names>." / English equivalent.runs/<today>/,审完手动上传到 Canvas (or your skill's submission flow)." / English equivalent.Render exactly one ## Next step block. Single sentence preferred; multiple bullets allowed only when there are multiple distinct actions of equal urgency. Keep the language matched to the rest of REPORT.md (Chinese if the run was triggered by a Chinese-language scan, English otherwise — same sniff rule as canvas-scan §5b).
For every item from this run whose final status is draft_ready or submitted and has a draft_path:
Map course → subfolder:
| skill / course pattern | subfolder |
|---|---|
code_py / the code course | final_drafts/CODE/ |
zybooks / the zyBooks math course | final_drafts/ZYBOOKS/ |
ac_english / the writing course | final_drafts/WRITING/ |
quiz / the quiz course | final_drafts/QUIZ/ |
| anything else | final_drafts/_other/ |
Derive filename: strip course prefix from assignment name, _ for spaces, keep extension.
Copy:
import shutil
from pathlib import Path
dest_dir = Path("final_drafts") / subfolder
dest_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(draft_path, dest_dir / short_name)Rewrite final_drafts/README.txt entirely:
filename STATUS (due DATE)✅ 已交 Canvas / ⏳ 待上传 / 🔧 待答题_processed.json for statusThe final_drafts/ folder is the CEO's easy-find drop zone for anything to upload. Skip this and drafts rot unseen.
(The .scan_in_progress marker is removed in §6 step 5 as the final step of finalize, not in a separate numbered section.)
Crash-path fallback: if dispatch crashed mid-run (sub-skill threw, Canvas API died, etc.) and you're recovering in a follow-up turn before marker removal: make sure every item in assignments.json has a result.json (write status: skipped, notes: "execute crashed at this item", deferred_to_next_run: true for unfinished ones). Then §6 step 5 removes marker. Then report the crash to the user with specifics.
user_decision isn't in {approve, swap:*}. "Not approved" means "not approved". Silent over-execution is the exact failure mode we're preventing./canvas-scan./canvas-scan.draft_path or status in the ledger. If a sub-skill returned error, record error — don't round up to draft_ready.SECRETS.md, the standing auto-submit authorizations are limited (the quiz course under CANVAS_QUIZ_AUTORUN=1, the code course under its own explicit flag). Everything else is draft-only; upload is the user's call.The old canvas-router used to do scan + dispatch + finalize all in one skill. It was split because a single SKILL.md's "stop and wait for approval" instruction was too easy to drift past. The split makes the approval gate a filesystem boundary (plan.json on disk between two Skill invocations) instead of a prose boundary (a paragraph in a SKILL.md).
Earlier versions of this (canvas-execute) skill also had a "pre-dispatch session-budget guard" that estimated total turns before dispatching and refused if >60. That was over-engineered: it fired after the user had already approved, creating a frustrating "batched, then blocked" loop. The real mechanism now (§5 mid-run pause-and-ask) is simpler: just do them one at a time, stop when it feels tight, report clearly, ask whether to continue.
Whenever you're tempted to "just also do the scan inline if plan.json is missing", remember: that temptation IS the bug. Hand back to the user, let them run /canvas-scan, start fresh with a real plan.
© X-isdoingreat, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .claude/skills/canvas-execute of X-isdoingreat/canvas-pilot.
Open the folder on GitHubat commit 6b79d5b
Canvas Execute 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 |
|---|---|---|---|---|---|---|
| Canvas Execute this skillX-isdoingreat/canvas-pilot | 125 | — | ~6.6k | Automated safety check: Notes | AGPL-3.0 | |
| Hook Development for Claude Code Pluginsanthropics/claude-plugins-official | 38k | 10 repos | ~4.1k | Automated safety check: Notes | Apache-2.0 | |
| Plugin Settings Patternanthropics/claude-plugins-official | 38k | 7 repos | ~3k | Automated safety check: Pass | Apache-2.0 | |
| Mole Bug Patternstw93/Mole | 70k | — | ~2k | Automated safety check: Pass | GPL-3.0 | |
| Neat-Freak Knowledge CloseoutKKKKhazix/khazix-skills | 21k | — | ~1.9k | Automated safety check: Pass | MIT | |
| E2Ecallstack/react-native-pager-view | 3.4k | 1 repos | ~2.1k | Automated safety check: Pass | MIT |
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
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.
tw93/Mole
A catalog of recurring bug shapes in the Mole Mac cleaner, used to review safety-sensitive diffs for deletion safety, unbounded commands, shell traps and weak tests.
KKKKhazix/khazix-skills
Brings project docs, agent rule files, authorized memory and leftover workspace files back in line with what the code and runtime actually do at the end of a work session.
callstack/react-native-pager-view
Agentic end-to-end tests with e2e, the e2e runner. An agent skill from callstack/react-native-pager-view.
automazeio/ccpm
Runs a spec-driven workflow from PRD to epic to GitHub issues to parallel agents, with status, standup and blocked-work reports from bundled scripts.
X-isdoingreat/canvas-pilot
A skill your agent uses when verified work from today or another day should become an X/Twitter post, build-in-public update, ship log, or bilingual draft.
X-isdoingreat/canvas-pilot
A skill your agent uses when a short local academic draft needs role-aware syntax diversification while preserving meaning, locks, source grounding, rubric-critical openings, and document structure.
X-isdoingreat/canvas-pilot
A skill your agent uses when managing Canvas Pilot schedules: install, inspect, pause, change, delete, or safely test scheduled scans and runs.
X-isdoingreat/canvas-pilot
A skill your agent uses for an approved long academic-writing assignment routed by canvas-execute after the deterministic writing router selects essay.
X-isdoingreat/canvas-pilot
A skill your agent uses for an approved Canvas assignment that no specialized course skill can handle.
X-isdoingreat/canvas-pilot
A skill your agent uses when a local academic draft needs a meaning-preserving humanizing pass with less uniform syntax while retaining rubric, source, lock, voice, and length constraints.
Works with
This skill should be used when executing an already-scanned Canvas plan after the user has approved it. Canvas Execute is an agent skill from X-isdoingreat/canvas-pilot. This skill should be used when executing an already-scanned Canvas plan after the user has approved it.
Run `npx skills add X-isdoingreat/canvas-pilot --skill canvas-execute -a claude-code`. Or copy the skill folder (.claude/skills/canvas-execute in X-isdoingreat/canvas-pilot) into .claude/skills/canvas-execute in your project. Claude Code loads it when a task matches its description.
Run `npx skills add X-isdoingreat/canvas-pilot --skill canvas-execute -a codex`. Or copy the skill folder (.claude/skills/canvas-execute in X-isdoingreat/canvas-pilot) into .agents/skills/canvas-execute 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 X-isdoingreat/canvas-pilot --skill canvas-execute -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/canvas-execute, .gemini/skills/canvas-execute, .github/skills/canvas-execute and .opencode/skills/canvas-execute in your project.
SKILL.md names no scripts, command-line tools or credentials: Canvas Execute is instructions for the agent only. Our summary lists: Python 3. Its frontmatter pre-approves these tools: Bash, Read, Write, Edit, Glob, Grep, WebFetch, Skill, TodoWrite.
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (mentions a .env file; pre-approves every shell command (allowed-tools: bash)), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Canvas Execute is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.6k tokens (SKILL.md is roughly 26k 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 Canvas Execute: Hook Development for Claude Code Plugins (anthropics/claude-plugins-official, 38k stars), Plugin Settings Pattern (anthropics/claude-plugins-official, 38k stars), Mole Bug Patterns (tw93/Mole, 70k stars) and Neat-Freak Knowledge Closeout (KKKKhazix/khazix-skills, 21k stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
X-isdoingreat (a GitHub user) maintains it in X-isdoingreat/canvas-pilot, which has 125 GitHub stars. The repository holds 32 skills in this directory. The repository was last updated on July 31, 2026.
Source: X-isdoingreat/canvas-pilot on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.