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 retryasks the questions you missed in the last quiz again, as the results screen already offeredSECURITY.mdwith private vulnerability reporting
Changed
canvas-cli statusshows the provider and model that will actually be used, notes whenAI_PROVIDERcomes from the environment, and says when its value is unrecognizedloginandlogoutuse the active profile (CANVAS_CLI_PROFILE, thendefault) when--profileis omitted, and re-runningloginkeeps settings the wizard doesn't ask about- The Bedrock step in
loginoffers the model list, with a custom id option, instead of a blank prompt ingestlists your courses when nothing matches, and its summary names what Canvas refused instead of a generic note--refreshdownloads 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
/qstarted 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
/doctorhint describe current behaviour - Saved API keys are found when
AI_PROVIDERuses 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_URLorOPENAI_BASE_URLfrom 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-cliwith 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], andexamples, plus global--debugand-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_HOMErespected) - 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;/filesand/modulesviews /timelineASCII Gantt view of upcoming work (week, month, semester, next N days/weeks,--all)/gradewith per-course summary, detail view, and a "need <letter>" calculator/quizpractice quizzes (count, difficulty, flashcard mode, topic) generated from course material/announcementsscrollable card view and/threaddiscussion 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/modelpicker for switching provider and model at runtime, with/model effortand/model keysubcommands and custom model IDs; friendly model names in the picker and header/doctordiagnostics for config, credentials, Canvas connectivity, and AI provider keys/loginfrom 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 modelauto, custom model IDs, andAI_EFFORTmapped to the CLI's--effortAI_PROVIDER=codex(aliaseschatgpt,openai-codex), experimental: use a ChatGPT plan through the OpenAI Codex CLI; default modeldefault(Codex's current default model; your own Codex config is not read), custom model IDs, andAI_EFFORTmapped tomodel_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; theloginwizard,/model,status, and/doctorunderstand subscription providers (CLI installed / signed-in checks, offer to runcopilot loginorcodex 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-Afterparsing, and AbortSignal support - Structured error types with user-facing messages and exit codes; Node.js version check at startup
--debugdiagnostic logging to stderr with automatic secret masking (also enabled byDEBUG=canvas-cli)- CI workflow (typecheck, test, build, audit), npm publish workflow with provenance,
prepublishOnlygate, 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 underattachments/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_replieslists), 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.jsonand 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 underattachments/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": falsein config.json) skips the request entirely - Ingestion: per-section, group, and student due/unlock/lock dates (
include[]=all_dateson the assignment list, no extra requests) recorded asdateDetailsinassignments.jsonand 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, andannouncements.jsonrecords 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_coursefinds "lecture 5 recording" and hands off toopen_lecture - Agent:
read_filetakessection("Page 57", "57", "p. 12", or a heading fragment, with a raw-heading fallback when the splitter folded the section away) andoffset; 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_workspaceandsearch_coursetake alimit(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
/workinvestigation'sread_documenttakes asectiontoo, 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 theeffortparameter (older Claude models keep an extended-thinking budget), OpenAI models getreasoning.effortincludingxhighandmax, Gemini models get athinking_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-5shows as "Opus 5" andgpt-5.6-terraas "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
defaultmodel 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 thecodexCLI runs); without it, behaviour is unchanged @ai-sdk/openaibumped to 3.0.104 somaxreasoning effort passes provider validation- The TUI is the sole interactive interface. There are no non-interactive
courses,assignments,show,do,work,ask,grades, orsubmitcommands. - 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_coursehits 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_workspacepreviews 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_coursepassage,search_workspacepreview 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_assignmentsandread_threadresults 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_announcementscarries a citable listing artifact but never counts as a full read, so the agent still follows it withread_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_assignmentsfor due-date questions,list_announcementsfor post questions, then a workspace and a course search, elselist_files); an announcement listing is followed byread_threadon 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_linkanchors, plain/courses/:id/files/:idand/files/:idlinks, and PDFs embedded through iframe/embed/objectsrc,data,data-srcanddata-download-url— with relative hrefs resolved against the Canvas base URL,/previewlinks 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
srcanddataattributes 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/.srtcaption 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 fromlanguage-*classes);<details>render as "Details: <summary>" blocks;<video>/<audio>list every<source>, caption/subtitle<track>, and poster;<object>/<embed>and lazydata-src/dataURLs 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.nodeis>=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 failedwith 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
Uint8Arrayview, 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
loginprompts 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=fileis set - When
CANVAS_BASE_URLcomes from the environment,CANVAS_ACCESS_TOKENmust 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_onlywith 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_KEYis stripped from the Codex environment so the subscription path never falls back to API billing