Safety Model
canvas-cli performs real Canvas writes (submissions, discussion replies, inbox messages, grading, content edits, raw API). Every write command enforces the safety gates below.
Safety levels
Read operations
Read operations can run normally with token authentication.
Examples:
- list courses
- list modules
- list assignments
- get assignment details
- download files
- export course context
Low-risk writes
Low-risk writes affect the authenticated user's own workflow or are easy to review.
Examples:
- send inbox message
- reply to discussion
- submit assignment
- add submission comment as self
Requirements:
- support
--dry-run - require explicit content input through flags/files/stdin
- emit audit log on execution
- reject empty body unless explicitly allowed
High-risk writes
High-risk writes affect students, grades, course content, publication state, dates, or many records.
Examples:
- set grades
- batch grade import
- update due dates
- publish/unpublish content
- edit pages
- create announcements
- delete files
- update modules
Requirements:
- default to dry-run preview where feasible
- require
--confirm - require operation count confirmation for bulk operations
- support
--read-onlyhard block - emit audit log
- preserve Canvas response metadata
- show partial failures clearly
There is no separate destructive tier: the CLI has no dedicated delete subcommands, so deletes (when needed) go through canvas api delete and are gated as high-risk writes. See the amendment in docs/adr/0002 for the history.
Global safety flags
--dry-run build and display operation without sending mutation
--confirm permit mutation
--read-only block all mutation commandsEnvironment variable:
CANVAS_READ_ONLY=1CANVAS_READ_ONLY=1 should override command flags and block writes.
--read-only behavior
When --read-only is set (via flag or CANVAS_READ_ONLY=1 env var):
- Any write command exits immediately with exit code 7
- The error message is:
"operation blocked by read-only mode" --read-onlyoverrides--confirm— even with--confirm, writes are blocked--dry-runis still allowed under--read-only(it doesn't perform writes)
Interaction between --dry-run, --confirm, and --read-only
| Flags | Behavior |
|---|---|
| (none) | Prompt user interactively |
--dry-run | Show preview, exit 0, no mutation |
--confirm | Execute without prompt |
--dry-run --confirm | Show preview, exit 0, no mutation |
--read-only | Block with exit 7 |
--read-only --confirm | Block with exit 7 |
--read-only --dry-run | Show preview, exit 0 (allowed) |
Audit log
Every remote mutation must append a local JSONL event.
Default path:
Linux: ~/.local/state/canvas-cli/audit.jsonl
macOS: ~/Library/Application Support/canvas-cli/audit.jsonl
Windows: %LOCALAPPDATA%\canvas-cli\audit.jsonlAudit event shape:
{
"time": "2026-06-12T19:20:00Z",
"command": "grade.set",
"profile": "default",
"base_url": "https://school.instructure.com",
"method": "PUT",
"path": "/api/v1/courses/123/assignments/456/submissions/789",
"resource": {
"course_id": "123",
"assignment_id": "456",
"user_id": "789"
},
"request_hash": "sha256:...",
"response_status": 200,
"canvas_request_id": "...",
"dry_run": false
}Never log tokens or full sensitive message bodies by default. Log hashes and metadata. Provide --audit-include-body only if explicitly added later and documented.
Dry-run output
Dry-run should show:
- intended method/path
- target resource IDs
- high-level diff or payload summary
- count of affected records
- whether the command would upload/download files
Dry-run must not send remote mutation requests.
Bulk operations
Bulk write commands must generate a plan first.
Example:
canvas grade import --course 123 --assignment 456 --csv grades.csv --dry-runThen:
canvas grade import --course 123 --assignment 456 --csv grades.csv --confirmPartial failure handling:
- continue or stop behavior must be explicit
- default should stop on first write failure for high-risk operations unless
--continue-on-erroris set - final JSON must include
ok: falseand categorypartial_failurewhen any item fails