A generic Canvas LMS CLI for teachers. Supports listing submissions, downloading student work, bulk grading, creating pages and assignments from markdown, uploading files, building course modules, and posting announcements.
# 1. Add the marketplace (one-time)
/plugin marketplace add guipaiva/canvas-cli
# 2. Install the plugin
/plugin install canvas-cli@canvas-cli
# 3. Install the Python package (required for the `canvas` command)
pip install -e ~/.claude/plugins/cache/canvas-cli/canvas-cli/*/
# 4. Reload plugins
/reload-pluginsWhy the pip step? Claude Code plugins don't auto-install Python packages. The
canvascommand must be on your PATH for the skill to work.
pip install git+https://github.com/guipaiva/canvas-cliOr for local development:
git clone https://github.com/guipaiva/canvas-cli
pip install -e ./canvas-cli-
API token — generate at Canvas → Account (avatar, top-left) → Settings → Approved Integrations → + New Access Token. Set it as an env var:
export CANVAS_API_TOKEN=your_token_hereOr add it to a
.envfile at your project root (loaded automatically). -
Project config — create
.canvas.tomlat your project root:[canvas] base_url = "https://your-institution.instructure.com" [aliases] "MY-COURSE-P" = 12345 "MY-COURSE-T" = 12346
The CLI walks up from the current directory to find the nearest
.canvas.toml.--courseaccepts either an alias or a raw numeric Canvas course ID.
canvas submissions --course MY-COURSE-P --assignment 250093canvas download --course MY-COURSE-P --assignment 250093 --out submissions/hw1Downloads all submission files into per-student directories. Emits students.json (name → Canvas user ID mapping). Re-running is safe — already-downloaded files are skipped. Handles group assignments correctly.
canvas assignments --course MY-COURSE-Tcanvas roster --course MY-COURSE-Tcanvas grade --course MY-COURSE-P --assignment 250093 --student "Alice Silva" --grade 8.5 --comment "Good work"
canvas grade ... --dry-run # preview without postingBulk post grades from a JSON file:
canvas post-grades \
--course MY-COURSE-P \
--assignment 250093 \
--grades grades.json \
--submissions-dir submissions/hw1 \
--dry-runInput schema (grades.json):
[
{
"student_name": "Alice Silva",
"score": 8.5,
"comment": "Good work. Exercise 3 used map() instead of reduce().",
"also_post_to": ["Bob Souza"]
}
]comment— fully-formatted string posted as a Canvas submission commentalso_post_to— list of student names; CLI fuzzy-matches and posts the same grade to each- Scores
< 0are skipped (upstream error sentinel) students.jsonfromcanvas downloadmust be present in--submissions-dir
See examples/grades.example.json for a full example.
canvas page-create --course MY-COURSE-T --title "Lecture 3 Notes" --from-file notes.mdCreates a draft page — publish manually in Canvas.
canvas page-update --course MY-COURSE-T --page-url "lecture-3-notes" --publish
canvas page-update --course MY-COURSE-T --page-url "lecture-3-notes" --title "New Title"canvas assign-create \
--course MY-COURSE-P \
--title "Homework 1" \
--from-file hw1.md \
--points 10 \
--due "2026-05-30 23:59"Creates a draft assignment — publish manually at class time.
canvas file-upload --course MY-COURSE-T --file "Lecture 3 slides.pdf" --folder SlidesUploads as unpublished (locked) — publish manually in Canvas. The --folder path is created if it doesn't exist, nested paths included. --on-duplicate overwrite (the default) replaces a same-named file in place, keeping its ID; pass --on-duplicate rename to keep both. --name overrides the display name.
canvas modules --course MY-COURSE-T
canvas modules --course MY-COURSE-T --no-itemsShows publish state (✓/○), item type, indent level and content IDs — the IDs you pass to module-item-add --content-id.
canvas module-create --course MY-COURSE-T --name "Block A — Fundamentals"Creates an unpublished module with no prerequisites and no sequential-progress requirement. Refuses to create a second module with the same name.
canvas module-item-add --course MY-COURSE-T --module "Block A" \
--type File --title "Lecture 3 slides" --content-id 4224744 --indent 1--module accepts a module ID or part of the module name. Each type takes exactly one content reference:
--type |
Content flag |
|---|---|
SubHeader |
(none) |
File, Assignment |
--content-id |
Page |
--page-url |
ExternalUrl |
--url (plus optional --new-tab) |
Warns when the target module is already published — the item becomes visible to students immediately.
Uploading a lecture's material and filing it under the aula's subheading:
canvas file-upload --course MY-COURSE-T --file "Lecture 3.pdf" --folder Slides
canvas assign-create --course MY-COURSE-T --title "Homework 3" --from-file hw3.md --points 10
canvas module-item-add --course MY-COURSE-T --module "Block A" --type SubHeader --title "Lecture 3 — Collections"
canvas module-item-add --course MY-COURSE-T --module "Block A" --type File --title "Slides — Lecture 3" --content-id <file id> --indent 1
canvas module-item-add --course MY-COURSE-T --module "Block A" --type Assignment --title "Homework 3" --content-id <assignment id> --indent 1Everything lands unpublished; publish at class time from Canvas.
canvas announce --course MY-COURSE-T --title "Class cancelled" --body "No class today."Announcements are published immediately and visible to students.
--base-url URL Override CANVAS_BASE_URL env var
--token TOKEN Override CANVAS_API_TOKEN env var
This repo ships as a Claude Code plugin. Clone it and install as a plugin to get the canvas skill in all your Claude Code sessions:
git clone https://github.com/guipaiva/canvas-cli ~/.claude/plugins/canvas-cli
pip install -e ~/.claude/plugins/canvas-cliMIT