Skip to main content

Pawtograder CLI

The Pawtograder CLI runs course operations from a terminal. It copies assignments between terms, imports and exports rubrics, ingests grading data, performs bulk student-repository maintenance, allocates grading work, and exports course data for analysis. Every command posts to a single cli edge function on your deployment and authenticates with an API token. This page is the complete reference. If you are course staff looking for the handful of tasks you run most often, start with the CLI page for course staff.
The published package is @pawtograder/cli version 0.2.0, licensed GPL-3.0-only. Commands and flags may still change.

Install

The CLI is published to npm as @pawtograder/cli. The binary is called pawtograder.
Node.js 20 or later is required. The repos commands and assignments copy also need git on your PATH and working SSH access to the course’s GitHub organization; repos copy-after-source-due additionally needs rsync.

Running from the platform repository

Inside a checkout of the platform repository, npm run cli -- runs the same CLI from source:
npm run cli:repos -- is a shortcut for npm run cli -- repos. Two differences between the two entry points matter in practice:
  • The in-repo entry point loads .env.local and .env. The published binary does not read .env files at all, so a variable that works under npm run cli has no effect on an installed pawtograder. Export it in your shell instead.
  • A source run reports 0.0.0-dev for --version. That is deliberate, so a run from a working tree is never mistaken for a release.
Examples on this page use pawtograder. Substitute npm run cli -- if you are working from the repository.

Invocation

Every command is a group plus an action:
Running pawtograder alone prints help and You must specify a command, then exits 1. A group with no action prints You must specify an action the same way.

Global flags

Two flags work everywhere, and only these two:
The CLI parses arguments in strict mode. A flag that the command you typed does not declare is a hard failure: Unknown argument: … and exit 1. There is no set of shared options that works everywhere. --json, --dry-run, -c/--class and -a/--assignment are all declared per command, so check the tables below before scripting a flag onto a command you have not run it on.

JSON output

--json prints the raw JSON response instead of the formatted table or plan, and silences every progress message, so stdout is pure JSON that you can pipe into jq. Error text still goes to stderr. These eleven commands accept --json: classes list, assignments list, flashcards list, rubrics list, rubrics import, submissions list, help-requests list, discussions list, reviews list, reviews assign, and repos list. Every other command rejects it, including classes show, assignments show, rubrics export, and all three export commands.

Authentication

Create an API token

Open the user menu (your avatar, top right) in the web app and click API Tokens. Under Create New Token, enter a Token Name, choose the CLI (Command Line) or MCP + CLI token type, and click Create Token. The token is shown once, starts with mcp_ whichever type you chose, and is valid for 90 days. Token creation requires an instructor or grader role in at least one class. Scopes come from the token type. A CLI (Command Line) token carries cli:read and cli:write; an MCP + CLI token carries those plus mcp:read and mcp:write. Read commands need cli:read and write commands need cli:write, so both types work for the whole CLI.

Log in

The CLI checks the token locally before contacting the server. An empty or whitespace-only value gives No token provided., a token that does not start with mcp_ gives Invalid token format. Pawtograder API tokens start with 'mcp_'., and Ctrl-C at the prompt gives Login cancelled. All three exit 1. On success, the token and API URL are stored in ~/.pawtograder/credentials.json at mode 0600, inside a ~/.pawtograder/ directory created at mode 0700. The file is written first and the token verified second; if verification fails the CLI restores the previous credentials, or deletes the file if there were none. A failed login never clobbers a working session.

What --url must point at

--url takes your deployment’s API gateway origin, not the hostname you use for the web app. Pointing it at the web host returns an HTML page, and the CLI says so: API returned an HTML page (HTTP <status>) from <url>. / That URL is serving the web app, not the 'cli' Edge Function. / Point --url at your deployment's API gateway origin (usually https://api.<hostname>).
The gateway is normally api. plus your web hostname. The /functions/v1/cli suffix is optional, because the CLI appends it for you. These two commands store an identical endpoint:
A bare hostname with no scheme gets https:// prepended, trailing slashes are stripped, and a value that cannot be parsed as a URL fails with Invalid API URL: <url>. If you are unsure of the origin, copy it out of the API Tokens dialog, which shows the full endpoint form. --url exists only on login. Every other command reads the stored value, so switching deployments means logging in again.

Check and clear credentials

whoami prints Email, Name, User ID, and the path to the credentials file. logout deletes the credentials file. Neither takes any flag beyond the globals.
whoami exits 0 even when it fails. It prints Not logged in. Run 'pawtograder login' to authenticate. both when no credentials exist and when the stored token is expired or revoked, so do not use it to detect an expired token. logout also always exits 0, including when there was nothing to delete.

Tokens in CI

--token is a flag on login, not on individual commands. There is no token environment variable and no way to pass a token to any other command. Run one login step and let the rest of the job read the credentials file:
assignments delete prompts for confirmation on stdin, so pass --force to it in any non-interactive job.

Environment variables

Every variable is optional and read at runtime.
Remember that the published binary ignores .env files. Export these in your shell.

Exit codes

The CLI exits 0 or 1. There are no other codes.
  • 0: the command succeeded. A cancelled assignments delete confirmation also exits 0, because nothing failed.
  • 1: any failure, including an unknown or missing flag, a client-side validation failure, an authentication or permission failure, an HTTP error, or an unexpected exception.
assignments copy, repos sync-grade-workflow, and repos copy-after-source-due print their full per-item report and only then exit 1 if anything failed. A non-zero exit from those three means at least one item failed; read the report to see which.

Error messages

The 403 text names scopes, but a role check can produce the same status. See Who can run what.

classes

classes list

Lists every class you have a staff role in.

classes show

Takes one positional argument, identifier, which can be a class ID, slug, or name. It accepts no flags, including no --json.

assignments

Everywhere below, -c/--class accepts a class ID, slug, or name, and an assignment identifier accepts an assignment ID or slug.

assignments list

assignments show

Takes a positional identifier plus -c/--class (required). No --json.

assignments copy

Copies assignments from one class to another, including rubrics, autograder configuration, self-review settings, and handout and solution repository content. Repository content is pushed with local SSH git rather than server-side, which is why --workdir exists.
Two rules are enforced before anything runs:
  • Exactly one of --assignment, --schedule, or --all is required. Otherwise: Must specify exactly one of: --assignment, --schedule, or --all.
  • --workdir is required unless --skip-repos or --dry-run is set: --workdir is required unless --skip-repos is set (needed to clone source/target repos locally).
--debug turns on timing logs on the server, not locally. For local HTTP logging, set DEBUG=1.
Re-running a copy is safe. Assignments that already exist in the target are reported as (existing, validated/fixed) rather than duplicated, so a timed-out run can simply be repeated.

Schedule CSV format

The CSV is parsed on your machine before any request, and the parsed rows are what gets sent. The header must contain either assignment_slug or assignment_title. The date columns release_date, due_date, and latest_due_date are all optional.
Dates accept ISO form (YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS) or US form (M/D/YY or MM/DD/YYYY). An empty date cell inherits the source assignment’s value. A UTF-8 byte order mark, which Excel adds, is stripped. Parse errors name the row, as in Row 3 release_date: ….
A header without assignment_slug or assignment_title is rejected with CSV must have either an "assignment_slug" or "assignment_title" column.

assignments delete

Deletes an assignment and everything attached to it: GitHub student, handout, and solution repositories, submissions, groups, exceptions and late tokens, review assignments, gradebook columns, and autograder configuration.
Without --force, the CLI prompts Are you sure you want to delete "<title>"? This action cannot be undone. [y/N]. Only y or yes proceeds; anything else prints Deletion cancelled. and exits 0. The server refuses the delete if any student repository has a commit beyond its initial commit.

surveys

surveys copy

Exactly one of --survey or --all is required: Must specify exactly one of: --survey or --all.
copy is the only surveys action. There is no surveys list.

flashcards

flashcards list

Takes -c/--class (required) and --json (default false).

flashcards copy

Exactly one of --deck or --all is required: Must specify exactly one of: --deck or --all. Note that the short alias here is -d, not -a.

rubrics

Rubrics are imported and exported as YML. All three actions require both -a/--assignment and -c/--class.

rubrics list

Also accepts --json (default false).

rubrics export

The short alias for --type is a capital -T. With no -o, the file is written to the current directory as <assignment>-<type>-rubric.yml, using the literal value you passed to --assignment, so -a 123 produces 123-grading-rubric.yml. Unlike the export commands in Exporting student data, this file is written with your normal umask rather than mode 0600.
--strip-ids produces a template, not a round-trip file. Import it and you get new rubric rows rather than updates to the existing ones.

rubrics import

Replaces one rubric on an assignment with the contents of a YML file.
The YML is parsed and validated locally before any request. Client-side failures, all exit 1, are File not found: <path>, Invalid YML: empty or invalid document, Invalid YML: missing 'name' field, Invalid YML: 'parts' must be an array, and a check that rejects NaN or Infinity anywhere in the tree. --verbose here is a local print flag and has nothing to do with the PAWTOGRADER_VERBOSE environment variable.

submissions

submissions export is covered in Exporting student data.

submissions list

submissions comments import and sync

Both actions read a JSON file of comments and write them onto submissions. They take an identical flag set, and the difference is what happens to comments that are absent from the payload:
  • import only inserts. Nothing is removed.
  • sync inserts, then soft-deletes rubric checks that are missing from the payload. This is destructive.
Exactly one of --author-profile-id or --rubric-part-id is required, on both actions. Passing both gives Use only one of --author-profile-id or --rubric-part-id; passing neither gives One of --author-profile-id or --rubric-part-id is required. Because the rule is enforced in the handler rather than by the parser, --help shows both as optional.
File not found: <path> is checked locally before the request. Both file shapes are normalized on your machine, so a legacy batch-results.json and a manifest behave the same from here on.

submissions artifacts import

Uploads artifact blobs, such as coverage reports, onto existing submissions.
The manifest is a JSON object with a non-empty artifacts array:
Each entry needs either content_base64 or content_file. A relative content_file path is resolved against the manifest’s own directory and base64-encoded locally. Missing pieces fail before upload with Manifest must include non-empty artifacts array, content_file not found: <path>, or Artifact <name> needs content_base64 or content_file.
If an artifact import times out, the CLI’s own advice is to lower --batch-size rather than raise the timeout. It warns when a single batch exceeds roughly 5 MiB of base64 payload.

help-requests

help-requests list

The default --limit is 100 here, unlike the 1000 used by submissions list and reviews list. When a page is truncated, the CLI prints the exact --offset to continue with. Columns are ID, Queue, Status, Student, Assignee, Created, and Request, with timestamps in the class’s time zone.

help-requests close

Sets a help request to a terminal status. This is the only way to set the closed status; nothing in the web interface does it.
Output is Help request <id>: <previous status> -> <new status>, followed by Resolution: and Notes: lines when you supplied them.
This command is instructor-only and has no --dry-run. It always writes. A grader running it gets API error: Help request not found: <id>, because the server deliberately reports a not-found rather than disclosing that the request exists.
Four behaviors are worth knowing before you script this:
  • The default status is closed, so a bare close --id 42 closes the request and records no resolution status or notes.
  • The resolution system message is only posted when a resolution status is set. Closing without --resolution-status produces no message in the request’s chat.
  • Closing also records one help-activity row per participating student. If that bookkeeping fails, the request is still closed and the CLI tells you to re-run with --force to retry. Participants already recorded are not duplicated, so --force doubles as the retry for the activity backfill.
  • Forcing a close on a request with a live video call leaves the call running, and the End Call button is disabled from then on. The CLI warns when this happens.
Without --force, a request that already has a terminal status, or that has a live video call, fails with an API error and exit 1.

discussions

discussions list

Lists discussion topics with thread and question counts. Takes -c/--class (required) and --json (default false). list is the only discussions action.

reviews

reviews list

--rubric has no default here, so all rounds are listed. On reviews assign it defaults to grading.

reviews assign

Creates review assignments, either balanced round-robin across the grader pool or from an explicit manifest.
--grader takes profile IDs, not emails or names, and is repeated once per grader. Non-submitters are excluded by default, matching the web bulk-assign page. --file conflicts with --grader, --by-part, and --include-non-submitters: --file supplies explicit assignments; it cannot be combined with --grader, --by-part, or --include-non-submitters. It works fine alongside --rubric, --due-date, --dry-run, and --json. The manifest is a JSON array:

How --due-date is interpreted

A string that already ends in Z or an offset such as -05:00 is passed through untouched. Anything else is interpreted by the server in the class’s time zone, and a bare date means the end of that day. For a New York course, --due-date 2026-09-15 is 23:59:59 Eastern, not midnight UTC. Append a time (2026-09-15T17:00) for a specific hour.

What the allocation guarantees

Re-running is safe. Existing assignments are reused rather than duplicated, already-assigned work is left alone, and an assignment covering the whole rubric counts as covering each of its parts, so --by-part will not re-deal work someone already holds. Allocation honors grading_conflicts and never assigns anyone their own submission. That applies to --file entries too, which are rejected rather than written. The report distinguishes new assignments from retargeted_stale ones, where an existing assignment was repointed at a newer submission and so is not new load on that grader. It separately reports skipped_already_assigned, excluded stubs, excluded dropped students, stale collisions, and any submission left with no eligible grader after conflicts.

repos

repos sync-grade-workflow and repos copy-after-source-due run git on your machine over SSH, not on the server. Both need working GitHub SSH access to the course organization and a writable --workdir. repos copy-after-source-due also needs rsync on your PATH. The API is called only for metadata.

repos list

Takes -c/--class (required), -a/--assignment (required), and --json (default false).

repos sync-grade-workflow

Copies .github/workflows/grade.yml from the handout repository into every student repository, then commits and pushes.
The --concurrency default is 2 here, not the 4 used by assignments copy. Per-repository failures are reported and set the exit code to 1 after the run finishes.

repos copy-after-source-due

Copies each student’s source-assignment repository tree into their target-assignment repository, once the source assignment’s due date has passed. This suits sequential assignments where students build on earlier work.
--mirror-delete deletes files in the target repository that are absent from the source. Run it with --dry-run first.
Per-repository failures are reported and set the exit code to 1 after the run finishes.

Exporting student data

Three commands write course data to disk: assessment export, submissions export, and assessment deanonymize. All three are gated by the same --identity contract, and all three can write student personal data. Read this section before running any of them.

The --identity contract

assessment export and submissions export both take --identity, with three modes: Validation happens before any network call, and each failure exits 1:
  • --identity raw requires --i-understand-pii to acknowledge that real student data will be written to disk
  • --identity hash requires --salt (any string of length >= 16)
  • --salt must be at least 16 characters
Neither token mode can be reversed offline. The server mixes a deployment secret into every token, and the salt is never written into any manifest, so a hash dump is not something its recipient can de-anonymize. Only an instructor, running assessment deanonymize against the same deployment with the same salt, can map tokens back to students.
Tokens from the default opaque mode can never be mapped back to students. The salt is random per run and assessment deanonymize accepts hash only. If you export with the default and later need names, your only option is to export again with --identity hash --salt <a salt you keep>. Decide which mode you need before the run, not after.
--identity pseudonymizes who a row is about. It does not sanitize what the row says. Section and lab names, group names, submission file paths, full source file contents, free-text grader comments, gradebook override notes, hint text and student rating comments, autograder test output, and hidden instructor build logs all come out verbatim in every mode. In a small lab section, a section name plus a handful of tokens can re-identify students.
Repository names and commit SHAs are tokenized in hash and opaque mode, because a repository name usually embeds a student’s GitHub handle and a SHA can be looked up on GitHub. In raw mode they are real. A few identifiers are raw in every mode because they are course metadata rather than student identity: assignment, rubric, and gradebook column IDs and names, and the discussion_thread_id and error_pin_id on engagement rows. The last two are joinable back to discussion content that identifiable students wrote, so treat them as identifying even in a tokenized dump.

Who is allowed to run these

--i-understand-pii is an acknowledgement, not an authorization. Any holder of a cli:read token with a grader role in the class, which normally includes undergraduate teaching assistants, can run assessment export or submissions export with --identity raw and write real names and emails to disk. Only assessment deanonymize is restricted to instructors. If that is not the access boundary your course wants, the control is which roles you grant, not which flags exist.

Output locations and file modes

All three commands write into the current working directory by default, under a timestamped name, and -o/--output overrides that. Directories are created at mode 0700 and files at mode 0600. Setting those modes is best-effort and a no-op on Windows.

Integrity checks and partial output

Both export commands stream NDJSON and verify what they received. Every stream must end with an {end} marker and the server’s reported counts must match the rows received, or the command aborts with Stream count mismatch for <field>: server reported X but received Y. The dump is incomplete; do not use it for analysis. or Server stream ended without an {end} marker — the dump may be incomplete. Both exit 1. These aborts exist so that an incomplete dump is not mistaken for a complete one.
A failed assessment export or submissions export leaves the partial output directory on disk. Nothing is cleaned up. Delete the directory before retrying, and never analyze a directory whose run ended in an error.
Transient stream failures, such as a dropped socket, an HTTP 503 or 504, or an edge-function CPU limit, are retried up to three attempts with exponential backoff. Authentication, validation, and count-mismatch errors are not retried. Retries are safe because tokens derive from the run’s salt, so a retried call returns identical data. assessment deanonymize behaves differently: it buffers the whole roster and writes the CSV only after the row count checks out, so a failed run leaves no partial file.

assessment export

Exports assessment data for a class: subjects, sections, assignments, rubrics, autograder configuration, submissions, scores, tests, hints, engagement, and gradebook cells.
Two combination rules apply: --skip-gradebook cannot be combined with --gradebook-column, and --instructor-build-output-from-sentinel requires --with-instructor-build-output. The output tree is:
Omitting --gradebook-column exports every column, including instructor-only ones. The default is maximal, not minimal. Use --skip-gradebook to leave the gradebook out.
Each run prints a dump_id, a fresh UUID that is sent with every request and recorded in manifest.json, which is useful when correlating an export with server logs. An -a or --gradebook-column selector that matches nothing produces a warning rather than an error, so a mistyped glob quietly exports less than you expected. Read the warnings before you trust the dump.
--with-test-output and --with-instructor-build-output are not governed by --identity. Test output and hidden build logs are free text that routinely contains file paths, repository names, and student code. The only sanitization applied to hidden build output rewrites path prefixes ahead of /pawtograder-grading to /anonymous; repository URLs elsewhere in a log, git author names, and handles in stack traces pass through. Turning either flag on widens the identity surface whatever --identity says.

submissions export

Exports submission metadata and, in JSON format, the source files themselves.
The layout depends on how many assignments matched. Exactly one match writes <output>/<slug>/; more than one adds a level, <output>/assignments/<slug>/. Script against the root manifest.json, which lists the assignments, rather than assuming a depth.
--format changes what is on disk, not only its shape. The CSV columns are fixed: submissions.csv holds submission,subject,group,ordinal,sha,is_active,created_at,repository,has_final_review, and files.csv holds submission,name,is_binary,file_size,mime_type,binary_omitted. Neither contains source code. If the code is the point of the export, use the default --format json. An -a selector matching nothing is a hard error here, No assignments matched the given selectors, unlike assessment export, which warns and continues.
The root manifest.json records include_files, exclude_files, all_submissions, and with_binary only when you set them, so it is a faithful record of the flags a dump was produced with.

assessment deanonymize

Writes a CSV mapping the subject tokens in a hash export back to student identifiers.
The CSV header is exactly:
One row is written per active student. The section columns hold section names, whereas an assessment export in hash mode holds section tokens, so this file is the join key for both.
This is the highest-risk command in the CLI. Its purpose is to write a roster of real names, emails, and SIS IDs to disk, keyed to the tokens in an otherwise pseudonymized dump. Anyone holding both an export directory and its roster CSV has the full raw dataset. It is instructor-only, whole-class, and has no --dry-run and no assignment filter. Before you run it, decide where the CSV will live and when you will delete it.
Keep the salt. Nothing records it: manifest.json stores the identity mode, dump ID, export timestamp, and class, but never the salt. Without the exact original string the tokens will not line up and the CSV is useless. There is also no --identity flag here, because only hash exports can be reversed at all.
Validation before any request: omitting --i-understand-pii gives --i-understand-pii is required — this command writes real student names, emails, and SIS ids to disk, and a short salt gives --salt must be at least 16 characters (must match the salt from the original export run).

Who can run what

Issuing any token requires an instructor or grader role in at least one class. Beyond that, every command checks your role in the class you named. Two commands are stricter than the rest: admin is deliberately not a CLI role. A site administrator with no instructor or grader role in a class cannot run CLI commands against it.

Troubleshooting

Unknown argument

The flag does not exist on the command you typed. Strict mode makes this fatal rather than ignoring it. Run pawtograder <group> <action> --help and compare, and check the tables above: several flags that look global, including --json and --dry-run, are per command.

Authentication and permission failures

Run pawtograder login again first, since a token lasts 90 days and whoami cannot tell you that yours has expired. If a 403 persists after a fresh login, check whether the command is one of the two instructor-only ones listed above rather than assuming a scope problem: the 403 text names scopes, but a role check produces the same status. help-requests close is the exception, because a non-instructor gets a not-found error there instead.

Timeouts on long operations

Copying many assignments, importing large artifacts, and exporting a whole class can all outrun a gateway timeout.
For artifact imports, lowering --batch-size helps more than a longer timeout. For assignment copies, re-running is safe: existing target assignments are validated and fixed rather than duplicated. For exports, the CLI already retries transient stream failures three times on its own.

Git and SSH failures

assignments copy and both repos commands run git locally over SSH. Confirm SSH access with ssh -T git@github.com, confirm you are a member of the course’s GitHub organization, and confirm --workdir exists and has room for a clone of every student repository. If clones fail intermittently, lower --concurrency to 2 or 1 and add --delay-ms 1000 to space out the batches. Connection resets during git operations are retried automatically.

Getting help

Help output wraps at 100 columns and every screen ends with a link to https://pawtograder.com.