BackResources

Changelog

What changed in each release of canvas-cli.

Every release, newest first, from CHANGELOG.md. Each version is also a GitHub release with its binaries.

Unreleased

Added

  • /quiz retry asks the questions you missed in the last quiz again, as the results screen already offered
  • SECURITY.md with private vulnerability reporting

Changed

  • canvas-cli status shows the provider and model that will actually be used, notes when AI_PROVIDER comes from the environment, and says when its value is unrecognized
  • login and logout use the active profile (CANVAS_CLI_PROFILE, then default) when --profile is omitted, and re-running login keeps settings the wizard doesn't ask about
  • The Bedrock step in login offers the model list, with a custom id option, instead of a blank prompt
  • ingest lists your courses when nothing matches, and its summary names what Canvas refused instead of a generic note
  • --refresh downloads every file again; without it, a file whose size changed on Canvas is fetched again
  • Course and global chat answers are checked for dates and figures that appear nowhere in what the assistant was shown, with the same "could not confirm" note workspace answers get
  • The npm package requires Node.js 20.10, matching package.json

Fixed

  • Answers no longer include the model's narration from before its tool calls
  • The course status line read "next due due in 2d"; submitted assignments are no longer labelled overdue in the picker
  • /q started a quiz instead of quitting in course and workspace scope; exact command and alias matches now sort first
  • Home and End work in common terminals; an @ inside a word, such as an email address, is no longer read as a pin
  • The home screen shows the real version; the course picker subtitle and /doctor hint describe current behaviour
  • Saved API keys are found when AI_PROVIDER uses an alias or capitals, and a saved model is not sent to a different provider
  • Workspaces no longer get an empty resources/ folder

Security

  • A saved AI key is never sent to an ANTHROPIC_BASE_URL or OPENAI_BASE_URL from the environment; set the key alongside it
  • On-demand file downloads get the same Canvas-origin check and size cap as ingestion
  • Prompts reach the Copilot CLI on stdin rather than the command line, and the Copilot and Codex processes no longer inherit the Canvas token or API-key-provider secrets
  • Secrets saved to the macOS Keychain no longer appear in the process list, and every write is read back
  • .canvas-cli/ is created readable only by you, and an existing one is tightened

Version 0.1.02026-09-23

Initial release of @reyabsaluja/canvas-cli.

Added

  • One-line install for macOS and Linux (curl -fsSL https://raw.githubusercontent.com/reyabsaluja/canvas-cli/main/install.sh | bash) that installs a standalone binary with no Node.js requirement, verified against the release's SHA256 checksums; binaries for macOS (arm64, x64) and Linux (x64, arm64, glibc and musl) are attached to each GitHub release
  • Interactive TUI (canvas-cli with no arguments) with three scopes — global, course, and workspace — and a persistent chat session per scope under .canvas-cli/chat-sessions/
  • AI chat agent with tools for reading cached course material, downloaded attachments, and zip archives on demand; streaming responses, Esc to interrupt, typing while streaming, and @<resource> pins to attach files to a prompt
  • Assignment workspaces: automatic ingestion and AI investigation on first open, producing assignment.md, plan.md, notes.md, workup.json, and extracted text; /overview, /requirements, /plan, /resources, /evidence, /status, /refresh, and stale-workspace detection
  • Setup commands: login (guided wizard with provider and model pickers), logout, status, ingest <course> [--refresh] [--json], clean [--all] [-y], and examples, plus global --debug and -V/--version
  • Named profiles (--profile, CANVAS_CLI_PROFILE) for multiple Canvas accounts
  • Credential storage in the macOS Keychain, with a permission-restricted file fallback under ~/.config/canvas-cli/credentials/ on other platforms (XDG_CONFIG_HOME respected)
  • Course ingestion into .canvas-cli/courses/: modules, pages, files, assignments, syllabus detection, attachment download and text extraction, lecture discovery, and external-link capture, with progress indicators and graceful Ctrl+C/SIGTERM handling
  • Pickers for /courses, /recent, /assignments, /open, and /manage-courses; /files and /modules views
  • /timeline ASCII Gantt view of upcoming work (week, month, semester, next N days/weeks, --all)
  • /grade with per-course summary, detail view, and a "need <letter>" calculator
  • /quiz practice quizzes (count, difficulty, flashcard mode, topic) generated from course material
  • /announcements scrollable card view and /thread discussion reader
  • /lecture (alias /lec) lecture lookup with retrieval-augmented answers
  • /pdf (alias /make-pdf) export of chat context to .canvas-cli/exports/, rendered with LaTeX via Tectonic when available (offers to install it)
  • /copy (and Ctrl+Y) to copy the last response, the last N, or the whole transcript
  • /model picker for switching provider and model at runtime, with /model effort and /model key subcommands and custom model IDs; friendly model names in the picker and header
  • /doctor diagnostics for config, credentials, Canvas connectivity, and AI provider keys
  • /login from inside the TUI, and a first-run login prompt
  • Multi-provider AI via the Vercel AI SDK: Anthropic, OpenAI, Google Gemini, and AWS Bedrock (AI_PROVIDER, AI_MODEL, AI_EFFORT), with auto-detection from whichever API key is present and graceful degradation when no provider is configured
  • AI_PROVIDER=copilot: use a GitHub Copilot subscription (including Copilot Free and the student Copilot Pro) through the GitHub Copilot CLI, with no API key; default model auto, custom model IDs, and AI_EFFORT mapped to the CLI's --effort
  • AI_PROVIDER=codex (aliases chatgpt, openai-codex), experimental: use a ChatGPT plan through the OpenAI Codex CLI; default model default (Codex's current default model; your own Codex config is not read), custom model IDs, and AI_EFFORT mapped to model_reasoning_effort. OpenAI has not published terms that explicitly cover third-party tools on a ChatGPT plan; usage counts against the user's plan. Claude Pro/Max is intentionally not supported per Anthropic's Agent SDK terms
  • Local MCP tool bridge (src/ai/mcp-bridge.ts) that exposes each request's Canvas tools to the vendor CLI over localhost, so subscription providers get the same tools as API-key providers; the login wizard, /model, status, and /doctor understand subscription providers (CLI installed / signed-in checks, offer to run copilot login or codex login). No new npm dependencies; subscription providers are never auto-detected
  • Canvas API client with retries and exponential backoff, per-request timeouts, proactive rate-limit throttling (including X-Request-Cost), Retry-After parsing, and AbortSignal support
  • Structured error types with user-facing messages and exit codes; Node.js version check at startup
  • --debug diagnostic logging to stderr with automatic secret masking (also enabled by DEBUG=canvas-cli)
  • CI workflow (typecheck, test, build, audit), npm publish workflow with provenance, prepublishOnly gate, and an end-to-end npx smoke test
  • Ingestion: folder-aware crawl of the whole Files tab (GET /folders), so lecture-like files anywhere in the course are indexed and downloaded under attachments/files/<folder>/, with a "Files-tab documents crawled" line in the summary
  • Ingestion: threaded discussion replies captured in thread order with participant names (nested replies, paged has_more_replies lists), and reply counts in the summary
  • Ingestion: files attached to announcements, discussion posts, and replies, and files attached directly to assignments (starter code, templates, data), are downloaded and extracted, deduped against the Files-tab crawl, with "files attached to posts" and "files attached to assignments" counts
  • Ingestion: lecture recordings embedded in pages, the syllabus, announcements, discussions, and assignment descriptions (YouTube, Panopto, Kaltura, Echo360, Zoom, Loom, Google, Canvas Studio) become lecture entries with title, host, and lecture number
  • Ingestion: quizzes (classic and New Quizzes, including practice quizzes and surveys) stored as "Quiz: <title>" pages with type, due/lock dates, time limit, attempts, points, question count, and instructions
  • Ingestion: external tools from course navigation (Piazza, Ed, Zoom, Gradescope, recording platforms, ...) captured as a "Course tools and external links" page with purpose hints and launch links
  • Ingestion: assignment groups (weights, drop rules, each assignment's share of the final grade) captured as a "Grading scheme: assignment groups and weights" page
  • Ingestion: module prerequisites, unlock dates, sequential progress, and per-item completion requirements (must view/submit/contribute, minimum score) recorded in modules.json and rendered as a "Requirements:" line on each module
  • Ingestion: grader feedback on the student's own submissions (GET /courses/:id/students/submissions, the caller's submissions only) — comment text, media comments, attached feedback files (downloaded under attachments/submission-comments/), and the rubric assessment (points out of the criterion maximum, rating, comment) — rendered as a "## Submission Feedback" section after each assignment's rubric; feedback text also seeds the page crawl, Canvas-file selection, and external-link capture. canvas-cli ingest --no-feedback (or "ingestSubmissionFeedback": false in config.json) skips the request entirely
  • Ingestion: per-section, group, and student due/unlock/lock dates (include[]=all_dates on the assignment list, no extra requests) recorded as dateDetails in assignments.json and rendered as an "## Assignment Dates" section listing every dated group, shown only for assignments that actually have overrides
  • Ingestion: announcement reply threads captured the same way as discussion threads (GET .../view with the entries fallback) and rendered under "## Replies" in each announcement extract; files attached to replies download under attachments/announcements/, reply HTML feeds Canvas-file selection, the page crawl, external-link capture, and lecture discovery, and announcements.json records reply and participant counts
  • Ingestion: course calendar events (GET /calendar_events?context_codes[]=course_<id>&all_events=true) stored as one "Calendar event: <title>" page each (start/end or all-day date, location, address, Canvas link, description) plus a chronological "Course calendar" page, so exam slots, review sessions and office hours are searchable; links in event descriptions feed the page crawl, file selection and external-link capture like any page
  • Extraction: DOCX, PPTX, and XLSX text extraction with headings, lists, tables, links, alt text, speaker notes, and sheets
  • Extraction: PDFs are extracted page by page under "## Page N" headings (cap raised from 30k to 400k characters, with a note naming the omitted page range); image-only pages keep their heading with an explicit "no extractable text" note so every page stays citable
  • Extraction: assignment extracts state submission rules (attempts allowed, group assignment and grading mode, peer reviews, anonymous grading, omitted from final grade, lock reason) plus the assignment group, its weight and drop rules, and the assignment's approximate share of the final grade
  • Extraction: page extracts carry "Updated:" and "Canvas URL:" lines, announcement extracts name their author and link, and announcement and discussion extracts list the files attached to the post and its replies
  • Retrieval: lecture entries (slides, recordings, embedded videos) are indexed as a "lecture" artifact kind, so search_course finds "lecture 5 recording" and hands off to open_lecture
  • Agent: read_file takes section ("Page 57", "57", "p. 12", or a heading fragment, with a raw-heading fallback when the splitter folded the section away) and offset; every read starts with the document's section outline, and the whole-read window is 120k characters (was 30k) with a cut-off note naming omitted sections
  • Agent: search_workspace and search_course take a limit (1-20, default 8) so a thin first pass can be widened
  • Agent: plan → investigate → reflect → decide loop with a visible 30-step budget and a per-result reflection footer that names the sections a cut-off read omitted
  • Agent: the /work investigation's read_document takes a section too, so workups can reach pages past its 60k cut-off
  • Grounding: section-level, answer-attributed citations for document reads and search hits ("Page 57", "## Grading")
  • Grounding: numeric and date claims in an answer (dates in any spelling, times, percentages, addresses, and spelled-out figures such as "ten percent") are checked against the evidence read; an unconfirmed figure lowers confidence one level and adds a "could not confirm" note in chat and workspace answers
  • Grounding: a weekday written next to a date is checked against the calendar ("Thursday March 27" is flagged when March 27 is a Friday)
  • Grounding: not-found answers name what was checked ("Not found after checking: Lab4.pdf (read in full); course search for "penalty" (no matches)")
  • Grounding: workups always report the document-stated due date and show a "Due-date conflict" line when it disagrees with Canvas

Changed

  • Model catalog refreshed for September 2026: Claude Fable 5.1, Fable 5, Opus 5, Sonnet 5, Opus 4.8 and Haiku 4.5; GPT-5.6 (Sol, Terra, Luna), GPT-6 Astra and GPT-5.5; Gemini 3.8 Flash, 3.7 Flash and 3.5 Flash Lite; and the matching Bedrock us. inference profiles. Defaults are now Claude Opus 5, GPT-5.6, Gemini 3.8 Flash, and Claude Sonnet 5 on Bedrock
  • Effort is a five-step scale (low, medium, high, xhigh, max) that works on every provider, including Google: Claude 4.6 and later use adaptive thinking with the effort parameter (older Claude models keep an extended-thinking budget), OpenAI models get reasoning.effort including xhigh and max, Gemini models get a thinking_level, and Bedrock mirrors Claude. Pickers only offer the levels a model accepts, and an unsupported level rounds up to the nearest one the model has
  • Model display names are derived from the id (so us.anthropic.claude-opus-5 shows as "Opus 5" and gpt-5.6-terra as "GPT 5.6 Terra") instead of a lookup table; the Bedrock picker reads from the shared catalog
  • ChatGPT via Codex: the model picker lists the models your Codex plan offers, and the default model now runs and displays Codex's current default model by name ("GPT 5.6 Sol (Codex default)" instead of "Codex default"), with the effort picker limited to the levels that model supports. The list comes from Codex's own local cache ($CODEX_HOME/models_cache.json, refreshed whenever the codex CLI runs); without it, behaviour is unchanged
  • @ai-sdk/openai bumped to 3.0.104 so max reasoning effort passes provider validation
  • The TUI is the sole interactive interface. There are no non-interactive courses, assignments, show, do, work, ask, grades, or submit commands.
  • Package published under the scoped name @reyabsaluja/canvas-cli; source maps and declarations are excluded from the tarball
  • Retrieval: every search path strips query stop words, applies conservative stemming, and expands course vocabulary synonyms (due/deadline, rubric/grading/marking, late/penalty, submit/upload, exam/midterm, lecture/slides, ...) at a lower weight so direct matches still win
  • Retrieval: synonym groups widened so "how much is this worth" reaches a "Points" section (grade/worth/points), "due date" reaches deadlines, "deduction" and "extend" reach the late-policy vocabulary, and starter/template/skeleton/scaffold/boilerplate map onto each other
  • Retrieval: search_course hits show the best-matching section and a query-centred passage instead of the document's opening characters, list up to two further matching sections of the same document ("also — Extensions: ..."), and rank a focused page above a long document that merely mentions every query word
  • Retrieval: search_workspace previews are a 2,400-character window centred on the matching passage; workspace extracted files are split on their markdown headings ("## Page N", DOCX/PPTX headings) so pages are searchable and citable inside workspaces; sections split on all six markdown heading levels (h5/h6 headings from HTML pages and deep DOCX outlines are their own sections, section: reads can target them, and heading-match relevance sees them); default match count raised from 5 to 8
  • Retrieval: when a long section mentions the query more than once, the search_course passage, search_workspace preview and remembered-read excerpt append up to two extra 160-character windows around the other mentions (joined with " … ", in document order) after the unchanged primary window, so a late policy that states the per-day rate, the cap and the extension rule thousands of characters apart shows all three
  • Retrieval: announcements and discussion threads get a recency boost (up to 20% for a post from today, fading to nothing at 90 days; discussions use the last reply time) so the newest matching post ranks first
  • Grounding: list_assignments and read_thread results now carry a citable evidence artifact and their rendered text, so an answer read from a discussion thread (an extension, a clarification) is cited by the thread's title at high confidence instead of "low, no sources" with a false "could not confirm" note; list_announcements carries a citable listing artifact but never counts as a full read, so the agent still follows it with read_thread
  • Agent: multi-part questions ("when is it due and how is it graded?", "both", a joiner plus two evidence topics such as due date and late policy) and broad preparation questions ("what should I know before starting?") now count as needing more than one grounded source, so the loop asks for a second relevant read instead of stopping at the first; observation relevance also scores an artifact's section label ("Late Policy") so a section-level search hit or read is matched to the question it answers
  • Agent: once a grounded read exists, the tool memory adds an evidence checkpoint instead of going silent: it counts the grounded sources, lists what they cover ("syllabus.pdf — Late Policy", with cut-off sections noted), asks the model to check every specific detail of the question (dates, points, names, steps) against that evidence and do a follow-up read when any is vague, and names up to two unread search hits as the likely next read (already-read and failed targets are skipped); search-result guidance now says unconditionally to read the best matching source rather than answer from snippets
  • Grounding: the evidence-backed answer prompt names each source's section ("lab4.txt — SUBMISSION") and carries its matched excerpt, and ends with four answer rules (cite at the section level, quote exact wording for requirements and dates, say what remains unclear, synthesise agreeing or conflicting sources); answers derived from the workup cite the documents in the workup's source trace instead of a generic "workup.json"
  • Agent: cross-turn tool memory grew from 2.4k/220-character head slices to 12k/1.2k-character excerpts centred on the current question; each remembered read states the section it covered and, for cut-off reads, the sections not read and the call to fetch them
  • Agent: the prompt teaches a figure check (every date, time, percentage, or value must come from a result read this turn), routes "how much is this worth", "where do I ask questions", and quiz-rule questions to the grading-scheme, course-tools, and quiz pages, and lists up to 60 lectures (was 30) and 8 lecture items per module (was 5)
  • Retrieval: questions about announcements, discussion threads and replies, lectures, recordings or slides, and what is due ("what's due this week", "list assignments") bypass the retrieval gate so the model reaches list_announcements/read_thread/open_lecture/list_assignments instead of answering from a remembered handout that merely mentions "extensions" or "lectures"; "prof", "professor", "instructor", "teacher" and "said" no longer count toward memory relevance
  • Retrieval: citation relevance scores each read against the answer as well as the question, so a document that supports the answer in the course's words ("late", "10% per day") when the student asked in their own ("hand it in on Saturday") still counts as a direct read (high confidence, no "matched search evidence" note) and a second document the answer draws on is still cited
  • Agent: when a question names a page or heading that a cut-off read never included, the retrieval gate issues a section read instead of answering from truncated memory; the numeric-claim check also accepts figures grounded by earlier turns' reads
  • Agent: a post-answer recovery pass runs after the model answers, outside the tool-step budget: an answer that states a date or figure from a search hit it never opened gets that hit read and the answer regenerated on the full text; a "not found" answer triggers the tools the model skipped (list_assignments for due-date questions, list_announcements for post questions, then a workspace and a course search, else list_files); an announcement listing is followed by read_thread on the best-matching unread post; a comparison grounded in one source searches for and reads the other side. Recovery calls show in the transcript like normal tool calls, a separator line is streamed before the regenerated answer, and verification runs on the regenerated answer
  • Grounding: the figure check is contextual when the evidence lists several dates or quantities of the same kind: the answer's figure must sit in an evidence line that shares an anchor ("Lab 4", "lab4.pdf") with the answer, and "Lab 4 is due March 20" is flagged as unconfirmed, naming the March 27 the Lab 4 line actually carries, instead of passing because March 20 appears somewhere in the schedule. An answer that says "must" or "required" is flagged when every matching evidence clause only says "may", "optional", or "recommended"
  • Extraction: HTML tables keep colspan/rowspan, nested tables, captions, and row-header keys; <dl> lists render as "term: definition" lines; links with generic text ("here", "download") show the linked filename
  • Extraction: zip summary caps raised from 30k to 120k characters per file and from 50k to 400k total, with an explicit "N more characters omitted" note instead of a silent cut
  • Extraction: zips inside zips are opened up to three levels deep — the archive summary inlines each inner archive's listing and text, and ingestion unpacks them into <entry>.zip.unpacked/ trees with per-file text sidecars so a rubric buried two archives down is searchable and openable; a fourth level is listed with a "nested more than 3 zips deep" note, and nested archives share the outer archive's entry-count and total-bytes caps
  • Discovery: Canvas file links are found in every form course HTML uses — instructure_file_link anchors, plain /courses/:id/files/:id and /files/:id links, and PDFs embedded through iframe/embed/object src, data, data-src and data-download-url — with relative hrefs resolved against the Canvas base URL, /preview links normalised, one file linked several ways downloaded once, and links to other hosts dropped at extraction time instead of becoming "failed" attachments; module "External URL" items that really point at a Canvas file are downloaded like module files (through the file metadata endpoint, or straight from the link when that is blocked)
  • Discovery: external-link capture now follows embedded resources, not just anchors — iframe/embed/object/video/audio/source src and data attributes and caption <track>s (titled from the track, e.g. "Captions: English (en)"), keeping the 4-wide concurrency, 30 s timeout and 100 MB body cap; Canvas external-tool launch URLs (/courses/:id/external_tools/:id) are never fetched, since Canvas answers them with its login page rather than the tool — the "Course tools and external links" page records the launch links instead
  • Extraction: external links to Office documents (.docx/.pptx/.xlsx, by extension or content type) are extracted with the Office extractor; .vtt/.srt caption tracks are captured as plain-text transcripts; Google Slides links are read through their .pptx export and Google Sheets through their CSV export, alongside the existing Google Docs text export
  • Extraction: Word documents include reviewer comments (listed under "Comments:" with [comment N] markers where they are anchored) and headers/footers; PowerPoint decks include legacy and threaded reviewer comments under the slide they belong to (unattached comment parts are listed after the slides); spreadsheet cells show their formula next to the cached value, e.g. 0.75 (formula: =SUM(B2:B3))
  • Extraction: HTML <pre> blocks become fenced code with indentation, tabs, and blank lines intact (with the language from language-* classes); <details> render as "Details: <summary>" blocks; <video>/<audio> list every <source>, caption/subtitle <track>, and poster; <object>/<embed> and lazy data-src/data URLs emit "Embedded object" lines with their type and fallback text
  • Workspace answers are no longer capped at 2-4 sentences, and the prompt asks the agent to list the sources it checked in not-found answers
  • Observation relevance stems question words and treats a match on a document heading as strong on its own, so a read that answers the question counts as grounded evidence across verification, memory reuse, and the retrieval gate
  • Minimum supported Node.js is now 20.10 (engines.node is >=20.10), the first release with stable JSON import attributes

Fixed

  • ChatGPT via Codex could not read the course at all on current Codex CLI builds: approval_policy="never" made Codex auto-deny every Canvas tool call, and GPT-5.6-class models hid the tools behind "code mode", so the model answered from the prompt alone ("descriptions weren't accessible in the course cache", workups saying PDFs were inaccessible). The bridge's tools are now pre-approved (mcp_servers.canvas.default_tools_approval_mode="approve"), code mode is turned off for the run, tool-call errors from the event stream are tracked, and a run whose tool calls were all refused fails with a clear message instead of returning an ungrounded answer
  • Endpoints that are rate-limited or blocked for students (Files, Pages) are skipped with a warning instead of aborting ingestion
  • Network failures from undici (fetch failed with an error cause) are classified as network errors with a useful hint
  • Assignments with a null score are treated as ungraded in /grade
  • Streamed output is preserved on Ctrl+C instead of wiping the conversation; rendering race between the spinner and the chat shell resolved
  • pdf.js is fed a Uint8Array view, fixing "bad XRef entry" failures on some generated PDFs
  • Files-tab file entries are deduped against their extracted attachment in search results
  • A due date correctly remembered from an earlier turn is no longer flagged as unconfirmed
  • A wrong due date is flagged even when its day number appears elsewhere in the evidence (dates are checked as month+day pairs in any common spelling)
  • Questions already covered by the workup still get tool calls instead of an answer from memory alone
  • Quizzes, external tools, and assignment groups degrade to none on a 403 instead of failing ingestion
  • Ctrl+C during the external-link capture phase of ingestion now aborts in-flight requests and stops starting new ones, instead of letting each request run to its 30 s timeout
  • login prompts no longer abort on arrow keys, other escape sequences, or a bracketed paste (only a bare Esc goes back), and a rejected token re-prompts in place with the error still visible and a "Try again or press esc to go back" hint

Security

  • File downloads send the Canvas token only to the configured Canvas origin (same-origin check)
  • Agent-initiated downloads are confined to the workspace directory; filenames and path segments are sanitized against traversal
  • Control characters are stripped from text before it reaches the terminal
  • On macOS, a successful Keychain write no longer leaves a plaintext copy on disk; the file backend is used only when the Keychain is unavailable or CANVAS_CLI_CREDENTIAL_BACKEND=file is set
  • When CANVAS_BASE_URL comes from the environment, CANVAS_ACCESS_TOKEN must too — a stored token is never sent to an environment-supplied URL
  • Profile names are validated to prevent path traversal; plain-HTTP Canvas URLs are rejected; secrets are masked in debug output
  • Zip inflation is bounded to stop archive bombs: 100 MB per entry (checked against the declared size and again while streaming), 5,000 entries and 1 GB inflated per archive, for zip summaries, on-demand unpacks (which now stream to a temp file and rename), and Office document containers. Oversized entries are still listed with their size and a "[Skipped ...]" note; everything else in the archive is read as before
  • External-link capture reads at most 100 MB per response; a larger resource is recorded as metadata_only with a note naming the limit rather than being buffered in memory
  • Subscription CLIs (Copilot, Codex) run isolated: non-interactive, in an empty temporary directory, with their built-in shell, file, and web tools removed and denied (Codex --ephemeral --sandbox read-only --ignore-user-config; Copilot with custom instructions and built-in MCP servers disabled). The Canvas tool bridge binds to 127.0.0.1 only and requires a per-run bearer token; OPENAI_API_KEY is stripped from the Codex environment so the subscription path never falls back to API billing