CLI Reference
Every command, flag, and environment variable in the newt CLI.
The newt CLI is the primary tool for authenticating, inspecting your API key, listing models, and installing the onboarding skill. It is a global command — installed once, used in any directory.
newt [command] [flags]Global flags
| Flag | Description |
|---|---|
-h, --help | Print usage and exit. Zero network calls, zero writes. |
-V, --version | Print newt <version> and exit. Also available as newt version. Zero network calls, zero writes. |
Help guard: newt --help and newt -h are safe to call anywhere — they make no network calls and write nothing to disk.
Commands
newt login
Authenticate with the New Theory console and save your API key.
newt login [--print]What it does: Opens an interactive browser pairing flow. Prints a URL and an 8-character code formatted XXXX-XXXX, then polls the console until you complete the authorization. On success, writes your API key to ~/.nt/credentials.
First-line output: Starting authentication…
The pairing URL and code appear on subsequent lines (~line 6). The load-bearing fact (the code itself) is not on line 1 — this is a known constraint of the interactive auth flow.
Flags:
| Flag | Description |
|---|---|
--print | Route auth output to stderr; print only the bare API key to stdout. Does not write ~/.nt/credentials. Use to pipe the key into another tool or capture it in a script. |
Exit codes:
| Code | Condition |
|---|---|
| 0 | Authentication succeeded |
| 1 | Network failure, pairing expired, or other error |
Network: POST to NT_CONSOLE_URL/api/cli/auth/start; polls NT_CONSOLE_URL/api/cli/auth/poll every 2 s for up to 10 minutes.
Writes: ~/.nt/credentials (mode 0600) on success — unless --print is set.
Help guard: newt login --help and newt login -h are safe — they print usage and exit 0 with zero network calls and zero writes. The guard shipped 2026-06-11 (commit 50004519, 44 tests).
newt logout
Remove your API key from disk.
newt logout [--json]What it does: Deletes ~/.nt/credentials and removes ~/.nt/ if it is now empty. Safe to call when already logged out.
First-line output (human): Logged out. or Already logged out — no credentials file found.
Flags:
| Flag | Description |
|---|---|
--json | Emit a JSON object instead of human-readable output. See output shape below. |
--json output shape:
{
"action": "removed" | "already_logged_out",
"credentials_path": "/Users/<user>/.nt/credentials",
"env_var_warning": true | false
}env_var_warning is true when NT_API_KEY is set in the environment — your key remains active even after logout.
Exit codes:
| Code | Condition |
|---|---|
| 0 | Always (idempotent) |
Network: None.
Writes: Deletes ~/.nt/credentials; removes ~/.nt/ if empty.
Help guard: newt logout --help and newt logout -h are safe — they print usage and exit 0 with zero network calls and zero writes. The guard shipped 2026-06-11 (commit 50004519, 44 tests).
newt create
Start a project from a starter kit.
newt create [template] [directory] [--no-git] [--setup] [--json] [-- kit arguments...]What it does: Fetches a starter kit at a pinned commit and unpacks it into a directory that is yours — no git remote pointing back at New Theory, and the driver versions the kit pinned already stamped. Run it with no arguments to list the templates your key can reach.
The directory defaults to the template name, and must not already exist with anything in it. By default the kit lands in a freshly initialized git repository with no commits and no remote: your first commit is the first commit in its history. --no-git leaves the directory inert.
Public kits need no key. Private kits are served by the console against your nt_ key — there is no GitHub permission to request and no SSH key to register.
The setup step belongs to the kit. Every question about your rig — arm addresses, camera serials, which values still need measuring — is asked by the kit's own setup step at scripts/setup, never by newt create. By default the command unpacks and prints that step as the next thing to run. --setup runs it instead, in the kit's directory, with its output passed straight through — including whatever it says about where it wrote its configuration. Nothing about that location is newt create's decision.
Kit arguments pass through untouched. Any argument newt create does not recognize is handed to the setup step verbatim, and asking for that implies --setup:
newt create <template> <dir> --some-kit-flag value --non-interactiveThe first unrecognized argument starts the handoff; everything after it goes to the kit, including flags newt create would otherwise own. Use -- to start the handoff explicitly when the kit wants an argument this command also owns:
newt create <template> <dir> --setup -- --json --non-interactiveWhich arguments a kit accepts is the kit's business — run its setup step with --help to see them. newt create keeps no list.
A kit that ships no scripts/setup is fine: the command says so and points at the kit's README. Asking such a kit to run one, or passing arguments nothing can consume, is a refusal rather than a silent drop.
Flags:
| Flag | Description |
|---|---|
--no-git | Skip git init — leave the directory as plain files. |
--setup | Run the kit's own setup step after unpacking. |
--json | Emit a JSON object instead of human-readable output. Cannot be combined with running the setup step — both write their report to stdout. |
-- | Everything after this goes to the kit's setup step. |
Exit codes:
| Code | Condition |
|---|---|
| 0 | The kit was written to the directory named in the output |
| 1 | Usage error |
| 2 | No template by that name — the available ones are printed |
| 3 | That template is private and no key was found |
| 4 | The console could not be reached, or answered an error |
| 5 | The console rejected your key |
| 6 | The directory already exists and is not empty — nothing was written |
| 7 | The kit's archive could not be downloaded |
| 8 | The downloaded archive was not the shape a repository archive has |
| 9 | The kit landed, but it declares no setup step to run |
| 10 | The kit landed, but its setup step could not be started |
| 11 | The kit landed, and its setup step exited non-zero |
| 12 | The registry's row for that kit is unusable — ours to fix, not yours |
| 13 | The console refused to serve the kit — nothing was downloaded |
Codes 9 through 11 all mean the kit itself is on disk and intact — re-running newt create is the wrong move, and each message says so.
Code 12 is not a failed download and there is nothing to retry: the registry named a public kit without saying where to fetch it from. newt upgrade may already carry the correction.
Codes 7 and 13 are the same disappointment from two different owners, and the split is what makes the number worth reading. 7 means GitHub would not hand over the archive. 13 means the console refused before GitHub was ever asked — a missing dispensing credential, a key it could not check — so there is no download to retry. Whatever the console said about it is printed verbatim, which is also how a cause the console adds later reaches you without an SDK release.
Network: GET to NT_CONSOLE_URL/api/cli/templates to resolve the template name, then a tarball download — direct from GitHub for a public kit, through NT_CONSOLE_URL/api/cli/templates/<name>/tarball for a private one. When the console is unreachable, public templates still resolve from a built-in table; private ones do not, and say so.
Writes: The output directory. With --setup, the kit's setup step runs and writes whatever it writes — read that kit's docs for where.
--json output shape: An object naming the template, its repo and pinned ref, the directory, the file count, the git state, a null remote, and a setup object — declared (the kit's setup path, or null), ran (always false under --json), and next (the command to run it yourself).
Help guard: newt create --help and newt create -h print usage and exit 0 with zero network calls and zero writes.
newt models
List models available to your API key.
newt models [--json] [--ids]What it does: Calls the New Theory registry and prints the model families your key has access to. Human output leads with a Key line naming which credential produced the listing (masked — never the full key) and its source (environment or credentials file), then the catalog grouped by base with its fine-tunes indented underneath. Raw uids are left out by default — the tag is the identity you scan by; --ids brings them back alongside each name.
First-line output (human): Key nt_••••••••<last8> (<source>) — the masked key identity and where it came from (environment or credentials file). The model catalog follows below it. If your key has no models, a message appears on stdout.
Example (default):
Key nt_••••••••5bde16a3 (environment)
ft-64cad948c4e9f09c-red-cube-bowl [base: ft_base_so101_ft]
ft-64cad948c4e9f09c-svla-so101-pickplace-20260714213146 [base: ft_base_so101_ft]
ft-64cad948c4e9f09c-my-first-pickplace [base: ft_base_so101_ft]
molmoact2
so101Example (--ids):
Key nt_••••••••5bde16a3 (environment)
ft-64cad948c4e9f09c-red-cube-bowl ft_64cad948c4e9f09c_31ac23c75952 [base: ft_base_so101_ft]
ft-64cad948c4e9f09c-svla-so101-pickplace-20260714213146 ft_64cad948c4e9f09c_e4519e2a61d5 [base: ft_base_so101_ft]
ft-64cad948c4e9f09c-my-first-pickplace ft_64cad948c4e9f09c_4527340bb316 [base: ft_base_so101_ft]
molmoact2 ft_base_molmoact2
so101 ft_6341c5_d13da9Flags:
| Flag | Description |
|---|---|
--json | Emit the raw model array from the registry as JSON. |
--ids | Show each model's uid alongside its name. |
--json output shape: An array of model objects. Shape is determined by the registry API.
Exit codes:
| Code | Condition |
|---|---|
| 0 | Models listed successfully |
| 1 | No API key, authentication failure, or registry unreachable |
Network: Registry call via newt.list_models(api_key).
Writes: None.
Help guard: newt models --help and newt models -h are safe — they print usage and exit 0 with zero network calls and zero writes. The guard shipped 2026-06-11 (commit 50004519, 44 tests).
newt status
Show your current authentication state and registry connectivity.
newt status [--json]What it does: Reports your key source (environment variable or credentials file), any active URL overrides, and whether the registry is reachable. Prints an amber warning block if NT_BOOTSTRAP_URL or NT_INFERENCE_URL is set.
First-line output (human): Key source: env_var / Key source: credentials_file / Key source: none. Registry reachability appears on a subsequent line (~line 4).
Flags:
| Flag | Description |
|---|---|
--json | Emit a JSON status object. |
--json output shape:
{
"key_source": "env_var" | "credentials_file" | "none",
"overrides": {
"NT_BOOTSTRAP_URL": null | "<url>",
"NT_INFERENCE_URL": null | "<url>"
},
"registry_reachable": true | false,
"latency_ms": <int> | null
}Exit codes:
| Code | Condition |
|---|---|
| 0 | Status check completed without error |
| 1 | No key, bad key, registry unreachable, or other error |
Network: Registry call via newt.list_models(api_key). Skipped when no key is present.
Writes: None.
Help guard: newt status --help and newt status -h are safe — they print usage and exit 0 with zero network calls and zero writes. The guard shipped 2026-06-11 (commit 50004519, 44 tests).
newt run
Run one real inference against your model and print what came back.
newt run <tag> [--snapshot <name>] [--prompt <text>] [--json]What it does: Loads a bundled recorded observation and calls your model once against prod via Robot(model=<tag>).infer(obs) — no hardware involved. Prints the resolved model, the round-trip latency, and the action-chunk shape. No robot is connected and nothing moves — this is a live inference against your model, not a robot demo, and the output says so plainly.
First-line output (human): The resolved model name.
Flags:
| Flag | Description |
|---|---|
--snapshot <name> | Bundled observation to send. Options are red_cube (6-axis SO-101), cup_stacking (8-axis rig), and pour_coffee_beans (8-axis rig). Defaults to the snapshot matching your model's contract. |
--prompt <text> | Override the snapshot's recorded prompt. |
--json | Emit machine-readable JSON instead of human output. |
--json output shape:
{
"tag": "<tag>",
"model": "<resolved model>",
"snapshot": "<name>",
"prompt": "<prompt sent>",
"latency_ms": <float>,
"total_ms": <float>,
"retries": <int>,
"action_chunk": {
"shape": [<int>, ...],
"axes": ["<axis>", ...]
}
}Exit codes:
| Code | Condition |
|---|---|
| 0 | Inference completed and result printed |
| 1 | No model tag given, unknown --snapshot, no API key, or the call failed (authentication, model not found, model not deployable, registry unreachable, contract mismatch, server error, verifier unavailable, or protocol error) |
Network: One inference call against prod via Robot(model=<tag>).infer(obs).
Writes: None.
Help guard: newt run --help and newt run -h are safe — they print usage and exit 0 with zero network calls and zero writes.
newt skill
Manage Claude Code skills provided by New Theory.
newt skill <subcommand> [-h | --help]Subcommands:
| Subcommand | Description |
|---|---|
install | Install the newt-onboarding skill into .claude/skills/ in the current directory. |
Help guard: newt skill --help and newt skill -h are safe — they print usage and exit 0 with zero network calls and zero writes.
Exit codes:
| Code | Condition |
|---|---|
| 0 | --help flag; or subcommand succeeded |
| 1 | Unknown subcommand |
Network: None.
Writes: None (for the skill dispatcher itself; install writes — see below).
newt skill install
Install the newt-onboarding skill into the current directory.
newt skill install [--json]What it does: Writes .claude/skills/newt-onboarding/SKILL.md into the current working directory, creating the directory if needed. If the file already exists, it is updated in-place.
First-line output (human): Skill installed — <path> or Skill updated — <path>
Flags:
| Flag | Description |
|---|---|
--json | Emit a JSON result object. |
--json output shape:
{"ok": true, "path": "/path/to/.claude/skills/newt-onboarding/SKILL.md", "overwrite": false}On error:
{"ok": false, "error": "<message>"}Exit codes:
| Code | Condition |
|---|---|
| 0 | Skill written or updated successfully |
| 1 | Read or write error |
Network: None. The skill file is read from package resources, not fetched over the network.
Writes: .claude/skills/newt-onboarding/SKILL.md relative to the current working directory.
Help guard: newt skill install --help and newt skill install -h are safe — they print usage and exit 0 with zero network calls and zero writes. The guard shipped 2026-06-11 (commit 50004519, 44 tests).
newt record
Record NT episodes from a robot (or a simulated joint stream) — the keyboard capture frontend on newt.recording.Session.
newt record --task <text> [--dest <dir>] [--simulate | --source <spec>] [--target <n>] [--hz <n>] [--json] [--teleop]What it does: Reads keystrokes and drives a recording session. SPACE starts and stops an episode; at the stop, ENTER keeps it, D discards, R redoes. A live frame counter and joint readout print during capture, and Ctrl+H is the kill — it torques off and leaves no partial episode directory. Every decision about episode format, atomicity, and the kill lives in newt.recording.Session; this command is only the skin. --teleop replaces this rhythm with a different one — see below.
Requires the recording extra: pip install "newt[recording]". Without it, the command stands down with the exact install line.
Flags:
| Flag | Description |
|---|---|
--task <text> | Language task prompt recorded in every episode. Required. |
--dest <dir> | Episode output directory (default: ./episodes). |
--simulate | Record from a fake joint stream — no hardware. Refused together with --teleop: there is no simulated demonstration. |
--source <spec> | The RecordingSource to record from — a short name your kit declares (e.g. live_pair), or a MODULE:FACTORY import path (e.g. mypkg.rig:make_source). Optional: with neither flag, the source is resolved from your rig's declarations (Source resolution). Mutually exclusive with --simulate. |
--target <n> | Stop after N kept episodes. Refused under --teleop — the composed path always records exactly one. |
--hz <n> | State sample rate (default: 30). Under --teleop, this is also the rig's drive rate — the tick that moves it is the tick that writes the frame. |
--author <text> / --license <text> | Provenance written to each episode.json. |
--json | Agent mode: line-delimited JSON events on stdout, line-delimited commands on stdin. Refused together with --teleop — that loop moves hardware and needs a keyboard for the kill key. |
--teleop | Compose recording with a live teleop session: drive the rig and record the same tick, in one process. TEMPORARY spelling, pending a naming decision — see below. |
--teleop: composes a live teleop session with a recording session in one process. Every tick, the source drives the rig and reports what it drove; that report is the frame the episode gets. One command in place of driving with newt teleop and capturing separately. The flag is marked TEMPORARY in newt record --help — the verb that names the end state, a recorded demonstration, hasn't been chosen yet. The flag, the demonstration config key, and the entry-point group it resolves against all move together when it lands.
--teleop clock: --hz (default 30) is the only rate you pass — it drives the rig and samples the episode on the same tick. The rig's own follower planner is built for its own rate, NT_TELEOP_RATE, which the source reads at construction rather than this command (also default 30). The two numbers are independent — set one without the other and the follower stutters — so the startup output prints both, before anything moves.
--teleop recorded action: the episode's leader channel holds the value the source actually sent to the follower on that tick, not a second read taken beside it.
--teleop source resolution: resolves [sources].demonstration in your site config, and the newt.sources.demonstration entry-point group in your kit — not [sources].record. On a rig that declares one, the command is bare — newt record --teleop --task "..." — and --source overrides it. See Source resolution for why it's a separate namespace.
--teleop capability declaration: a source is refused before it is constructed unless its factory is decorated @newt.teleop.drives_and_records. Point --teleop at a recording-only factory and it refuses with nothing connected and nothing energized — this is the error you hit if [sources].demonstration names a factory that only reads the rig.
--teleop terminal: same requirement as newt teleop — this loop moves hardware, so it needs a keyboard for the kill key. --json is not a substitute and is refused for the same reason.
--teleop exit grammar: inherited from newt teleop, not plain record's. Ctrl+C ends the session, puts the rig away by its declared rest, then de-energizes — and keeps the episode. Ctrl+H de-energizes in place with no motion first, and discards it. Which one happened is printed at the moment it happens. Plain newt record is unchanged — Ctrl+H de-energizes without resting — and the SPACE/ENTER/D/R rhythm doesn't apply under --teleop: one episode per run, ended by the same two keys that end a teleop session.
Exit codes:
| Code | Condition |
|---|---|
| 0 | Session ran and closed cleanly |
| 130 | Ctrl+H kill (no partial episode left behind) |
| 1 | Missing --task, no writable destination, or the recording extra is not installed |
| 2 | Refused before anything was built — stdin is not a TTY and --json was not set, or no source resolved (see Source resolution) |
Exit codes under --teleop:
| Code | Condition |
|---|---|
| 0 | Ctrl+C ended the session — the rig was put away and the episode kept |
| 130 | Ctrl+H fired (episode discarded), or Ctrl+C landed during bring-up (nothing had been recorded yet) |
| 1 | Usage error — --simulate, --json, --target, or a simulate-only flag passed alongside --teleop — or the source's factory could not be imported, raised while building, or a halt went unconfirmed |
| 2 | Refused before anything connected — no TTY, the kill key could not arm, no source resolved, or the source doesn't declare drives_and_records |
| 3 | The rig ended cleanly but the episode was refused (e.g. a camera failure) |
Network: None. Writes: episode_<id> directories under --dest.
Non-TTY without --json: stands down loudly — there is no keyboard to read.
Plain newt record and newt teleop are unchanged by any of this. Plain newt record is still how you capture from a source nobody is driving — a fixed camera, a sensor feed, a source with no leader to follow.
newt teleop
Drive one embodiment from another's motion — the keyboard frontend on a teleoperation session.
newt teleop [--source <spec>] [--rate <hz>]What it does: Every tick, the source produces an action and the sink is sent it unchanged — no scaling, no filtering, no interpolation. Your source factory owns the hardware: which devices, how they are reached, what an action is, and what "put yourself away" means. The command owns the tick, the kill, and which exit the session is on.
The two exits differ by one thing — whether anything moves first. Ctrl+H de-energizes immediately and does not move first; motion after the panic key is what the panic key exists to prevent. Ctrl+C ends the session the other way: the embodiment is put away by its own declared rest, then de-energized.
Needs a real terminal. With no keyboard there is no Ctrl+H, so the command stands down before anything connects rather than driving hardware you cannot stop. To bound a scripted session instead, wrap it: timeout --signal=INT 30 newt teleop — SIGINT lands where Ctrl+C lands.
Flags:
| Flag | Description |
|---|---|
--source <spec> | The teleop source to drive from — a short name your kit declares (e.g. live_pair), or a MODULE:FACTORY import path (e.g. mypkg.rig:make_teleop). Optional: without it, the source is resolved from your rig's declarations (Source resolution). There is no simulated mode to fall back to, so when nothing resolves the command refuses. |
--rate <hz> | Ticks per second (default: 30). The source is built at the rate it is driven at — one decision, not two. |
Exit codes:
| Code | Condition |
|---|---|
| 0 | Ctrl+C ended the session and everything confirmed it was de-energized |
| 130 | Ctrl+H fired, or Ctrl+C landed during bring-up |
| 2 | Stood down before anything connected — stdin is not a TTY, the Ctrl+H listener could not arm, or --rate was not a positive number |
| 1 | Usage error, no source resolved, the source refused to come up, or a halt went unconfirmed |
Network: None — no API key, no inference, nothing beyond your own hardware. Writes: nothing.
newt rest
Put the embodiment away, and report where each part actually ended up.
newt rest [--source <spec>]What it does: Asks your embodiment to run its own declared rest sequence, then leaves every part de-energized and prints the state each part reports back. Reach for it after a session was killed or faulted — when there is no clean exit path left to ride and the hardware is wherever it stopped.
The rig declares what "away" means; the command never picks a pose. Your parts declare rest_sequence() — the moves, in order, each with a name and a run(). The command decides when, in what order, and what must be confirmed. A part that declares no rest sequence is refused by name, before anything moves. There is no default pose and no fallback: driving hardware to a position nobody chose for it is the failure this refusal exists to prevent.
It reports what it read, not what it assumes. The final state of each part comes from that part's own report. A part that cannot be asked, or that went unanswered, is called out explicitly and exits non-zero — never "rested".
It is not a calibration command. No set-zero, no factory reset, no jigs. Nothing it does changes what an arm believes about its own zero.
Flags:
| Flag | Description |
|---|---|
--source <spec> | The rest source to put away — a short name your kit declares (e.g. live_pair), or a MODULE:FACTORY import path (e.g. mypkg.rig:make_rig). Optional, and on a configured rig you never pass it: without it, the source is resolved from your rig's declarations (Source resolution). There is no simulated mode to fall back to, so when nothing resolves the command refuses. |
Exit codes:
| Code | Condition |
|---|---|
| 0 | Every part ran its declared sequence, was de-energized, and said what state it ended in — that answer is printed for you, never graded here |
| 1 | Usage error, no source resolved, or the source refused to come up |
| 2 | Refused before anything was commanded — a part declares no rest sequence |
| 3 | A declared step failed; that part did not finish its sequence |
| 4 | A part went unanswered, or would not say what state it ended in — treat it as still holding until you have checked it yourself |
| 130 | Interrupted (Ctrl+C) — the run did not finish |
Network: None — no API key, no inference, nothing beyond your own hardware. Writes: nothing.
newt getip
Print this machine's LAN address(es) — what to paste into the console's rig address field.
newt getip [--json]What it does: Reads the machine's network interfaces and prints every address a browser on the same network could reach, labeled by interface, best guess first. Nothing is hidden: a machine with a Wi-Fi address, a VPN tunnel, and two IPv6 addresses prints all four and ranks them, because the machine that needs the second-place line is exactly the machine that would be stuck if it were dropped.
Best guess means the address this machine would actually send from. The top line is the source address of the default route, when the OS reports one; everything below is ordered by heuristic (up before down, IPv4 before IPv6, physical before tunnel or container interfaces).
It does not pick a port, and says so. The port is whatever your motion publisher printed when it started, and the CLI has no way to know it. The output ends with the paste shape — <address>:<port> — and leaves the port to you. IPv6 addresses print bracketed ([fd00::1]) so appending :<port> still parses.
It is a stopgap. Today you read the address off this verb and type it into the console yourself. Once a rig announces its own address, newt getip stops being a step in anyone's path and becomes a diagnostic — what you run when the automatic path has gone dark.
Flags:
| Flag | Description |
|---|---|
--json | Emit the same candidate list machine-readably. |
--json output shape:
{
"addresses": [
{
"address": "<ip>",
"interface": "<name>",
"family": "ipv4" | "ipv6",
"up": true | false,
"best_guess": true | false
}
],
"error_kind": null | "no_interface" | "unreadable_platform"
}Exit codes:
| Code | Condition |
|---|---|
| 0 | At least one reachable address was found and printed |
| 1 | Every interface is loopback or link-local (the machine can currently reach only itself), or the platform would not hand over its interface list at all — two distinct refusals, each naming what to do next |
Output streams: Addresses go to stdout; refusals go to stderr. ADDR=$(newt getip) captures an address or nothing — never a refusal masquerading as one.
Network: None. A UDP socket is connected to RFC 5737 documentation space to ask the kernel which interface it would route from; no packet is sent and nothing is contacted.
Writes: None.
newt finetune
Launch a training run on New Theory's GPUs with your API key, then watch it to completion.
newt finetune (--dataset <name> | --handle <job> | --list) [--status] [--steps <int>] [--name <slug>] [--fresh] [--json]What it does: Sends your dataset to the New Theory console, which launches the training job server-side on New Theory's GPUs and returns a job handle. No infrastructure credential ever reaches your machine. The CLI then polls the run until it reaches a terminal state, printing the resulting model tag and report-card pointer on success, or the pipeline gate that failed. --handle re-attaches to a run you already launched (poll only, no new launch); --status prints that run's current state once and exits; --list prints your recent runs.
First-line output (human): Launched fine-tune on dataset '<name>'. followed by the job handle. When re-attaching, the first line is Watching <job> ….
Flags:
| Flag | Description |
|---|---|
--dataset <name> | The staged dataset to fine-tune on. Launches a new run. |
--handle <job> | Re-attach to a run already launched, and poll it. |
--status | With --handle, print the run's current state once and exit instead of watching. |
--list | List your recent runs — handle, dataset, last recorded state, and when. |
--steps <int> | Advanced. Total training steps for this run, used with --dataset. A whole number between 2000 and 100000. Optional; omit it for the default. |
--name <slug> | Advanced. Name for the model this run produces, used with --dataset. A slug — lowercase letters, digits, and hyphens, 3–40 characters. Optional; omit it to name the model after the dataset. |
--fresh | Advanced. Ignore any existing checkpoint and retrain from scratch, used with --dataset. Optional; omit it to resume a completed run's finishing steps. |
--json | Emit machine-readable JSON instead of human output. |
--steps bounds: A value outside 2000–100000 is rejected with an error naming the bounds, never clamped into range. The value is recorded on the run. Leave --steps off for the default step count.
--name validation and collision: A name is validated as a slug — lowercase letters, digits, and hyphens, 3–40 characters — before any network call; anything else is rejected with an error, never reshaped to fit. If a model of that name already exists under your account, the launch is refused with a 409 naming the tag, before any training starts and before any GPU time is spent. Omit --name to name the model after its dataset (the default).
--fresh behavior: By default a re-run resumes a completed run's finishing steps instead of retraining. --fresh forces the full retrain from scratch, ignoring any checkpoint. Either way, the launch output names which path ran — resume or forced retrain — so the choice is never silent.
--json output shape (launch, --handle, --status):
{
"job_handle": "<id>",
"status": "succeeded" | "failed",
"gate": null | "<failing gate>",
"tag": null | "<model tag>",
"report_card": null | "<pointer>",
"steps": null | <int>,
"name": null | "<slug>"
}steps is the value set with --steps, or null when none was set (the server default is in force). name is the value set with --name, or null when none was set (the model is named after its dataset).
--list --json output shape:
[
{
"job_handle": "<id>",
"dataset": "<name>",
"status": "<last recorded state>",
"created_at": "<ISO 8601>",
"model_status": null | "pending" | "probed" | "live" | "dead"
}
]model_status is each run's model lifecycle state: null before the model registers, pending/probed while New Theory verifies the weights load, live once they do, and dead if they don't. A dead model is recoverable and stays listed.
Each run also carries a stable model identity (uid), assigned at launch and returned by the finetune status and jobs APIs. Bring it to the support table if you need help with a specific run.
Exit codes:
| Code | Condition |
|---|---|
| 0 | Run reached succeeded (or --status/--list completed) |
| 1 | No API key, launch rejected, run failed, or the poll gave up |
| 130 | You stopped watching with Ctrl-C (the run keeps going) |
Network: POST /api/finetune to launch, GET /api/finetune/status to poll a handle, and GET /api/finetune/jobs for --list. The console holds the training credentials; the CLI only ever sees a job handle.
Writes: None.
newt promote
Keep a fine-tune's checkpoint band and serve it — the CLI twin of the console's promote button.
newt promote <job-handle> --band <token> [--json]What it does: Registers one of a run's evaluated checkpoint bands as a served model, over the same route the console's promote button calls. The model is born pending; New Theory's admission chain runs a safety check and takes it live, usually within a few minutes. The output names the new model and points at newt models to watch it. Handle-only: list your runs with newt finetune --list.
First-line output (human): Promoted — your checkpoint is registered as a model. followed by the model tag and its pending status.
Flags:
| Flag | Description |
|---|---|
<job-handle> | The run whose checkpoint to serve (required, first argument). |
--band <token> | Which checkpoint band to serve — the evaluated step, passed verbatim (e.g. 010000). Required. |
--json | Emit the route's JSON response body on stdout (the registered model, or the server's refusal detail). |
Refusals: Every server refusal prints its plain reason verbatim — a band whose eval hasn't completed, a checkpoint whose location the training pipeline hasn't reported yet, or a run that already has a registered model (with that model's identity). A refusal is never collapsed into a generic failure.
Exit codes:
| Code | Condition |
|---|---|
| 0 | The band was registered (model born pending) |
| 1 | No API key, a bad argument, or the server refused the promote |
Network: POST /api/finetune/runs/<job-handle>/promote. Writes: None.
newt episodes
Inspect recorded episodes, push them up, and pull a staged dataset back down.
newt episodes validate <dir> [--json]
newt episodes push [dir] --dataset <name> [--json]
newt episodes pull <dataset> [--dest DIR] [--json]validate calls newt.recording.validate on an episode_<id> directory and renders the verdict — a PASS/FAIL line plus one line per invariant check. Frontend only: the checks themselves are the library's. Requires the recording extra: pip install "newt[recording]".
push uploads the episodes newt record wrote into your NT namespace, under a dataset name you pick. It walks dir (default ./episodes, the same place newt record --dest defaults to) for episode_<id> directories, hands each to the SDK's cloud sink, and writes the dataset's manifest.json last — the marker that says the dataset is complete. Bytes go straight to storage over signed links, never through the console. Progress is reported by episodes completed, never a percentage. No recording extra needed: push moves files, it doesn't read MCAP.
Before it signs anything, push prints a preflight — episode count, task, file count, total size, and the dataset name — and it refuses on that report rather than partway through an upload. Uploads are write-once: a dataset name can't be reused, added to, or deleted once any object lands under it, so a refusal that arrives mid-push costs you a name. push therefore checks the name is free (owner-scoped listing) and that the directory holds exactly one task before the first byte moves. If a push does die mid-flight, it says how many episodes landed, that the dataset is now partial, and that the name is spent — recovery is a new name.
pull downloads a staged dataset from your NT namespace. It fetches the owner-scoped download manifest (authed with your nt_ key), then downloads each object straight from storage over signed links — the bytes go GCS → your machine, never through the console. Files land under --dest (default ./<dataset>), recreating the dataset's relative layout. Progress is reported by files completed, never a percentage. Reruns are resumable: a file already present at the manifest's size is skipped, so an interrupted pull resumes cleanly.
Subcommands:
| Subcommand | Description |
|---|---|
validate <dir> | Validate an episode_<id> directory. |
push [dir] | Upload recorded episodes (default: ./episodes) into your NT namespace. |
pull <dataset> | Download a staged dataset into --dest (default ./<dataset>). |
Flags:
| Flag | Description |
|---|---|
--dataset <name> | (push) The name to land the episodes under. Required — never guessed from the directory name, because the name is write-once. |
--dest <dir> | (pull) Where to write the dataset. Default: ./<dataset>. |
--json | Emit machine-readable JSON (validate: the verdict; push: dataset, source, namespace, task, episodes, files, bytes; pull: total_files, downloaded, skipped, bytes). Under --json the result object is the only thing on stdout; the preflight and progress go to stderr. |
Exit codes:
| Code | Condition |
|---|---|
| 0 | Episode is valid / the push completed (manifest written) / the pull completed |
| 1 | A check failed, no argument given, an unknown subcommand, the recording extra is missing (validate); no key, no --dataset, nothing to push, the directory holds more than one task, the name is already taken, or an upload failed (push); the download failed / no key / dataset not found (pull) |
Network: validate none; push reads the console's owner-scoped listing (GET /api/uploads/list) for its preflight, then signs per file (POST /api/uploads/sign) and PUTs to storage; pull reads the console manifest + downloads from storage. Writes: validate and push none locally — push writes only to your NT namespace; pull writes files under --dest.
Help guard: newt episodes --help and newt episodes -h print usage and exit 0.
newt upgrade
newt upgrade [--print]Upgrade the CLI to the latest version. When newt was installed as a uv tool (the documented install), this runs uv tool upgrade newt for you and streams its output. When the install method can't be confirmed, it prints the command instead of running it — it never guess-runs a package manager against the wrong environment.
Flags:
| Flag | Description |
|---|---|
--print | Print the upgrade command and exit — never runs it. |
Passive update notice: After any command succeeds, the CLI may print one quiet line to stderr when a newer version is available — newt <latest> available — run 'newt upgrade'. It runs at most once a day, never blocks or slows a command, is silent on any network failure, and is skipped entirely under --json. Disable it with NEWT_NO_UPDATE_CHECK=1.
Exit codes:
| Code | Condition |
|---|---|
| 0 | The upgrade ran and succeeded, or the command was printed (unconfirmed install) |
| 1 | The upgrade command failed, or uv was not found |
Network: GET /api/cli/version (public, to source the command). Writes: ~/.nt/update-check.json (the once-a-day timestamp cache).
newt version
newt versionPrints newt <version> and exits. Identical to newt --version and newt -V.
| Exit code | Meaning |
|---|---|
| 0 | Always |
Network: None. Writes: None.
Environment and credentials
API key resolution
The CLI and SDK resolve your API key in this order, stopping at the first hit:
NT_API_KEYenvironment variable~/.nt/credentialsfile
NT_API_KEY always wins over the credentials file.
~/.nt/credentials
Written by newt login. Deleted by newt logout.
| Property | Value |
|---|---|
| Path | ~/.nt/credentials |
| Mode | 0600 |
| Format | api_key = nt_<hex> |
Source resolution
newt record, newt teleop, newt rest, and newt record --teleop each need a source — the factory that builds your robot. --source names one, and on a configured rig you never pass it. newt record --teleop resolves in its own namespace, demonstration, not record — a factory that reads a rig and a factory that drives it while reading it are different code, and a kit that declared one has not declared the other.
Two surfaces declare sources, and they answer different questions:
| Surface | Answers | Changed by |
|---|---|---|
[sources] in your site config | which source this bench uses by default, per verb | you, no reinstall |
newt.sources.<verb> entry points in an installed kit | which sources that kit offers, and the short names they answer to | the kit author; takes effect at install |
Resolution order, stopping at the first hit:
--sourcecontaining a colon — aMODULE:FACTORYimport path, imported directly. Neither declaration surface is consulted; this is the escape hatch and it answers to nothing.--sourcewithout a colon — a short name, looked up in what installed kits declare for the verb you ran. The verb is the namespace, sonewt rest --source live_pairis unambiguous even when four verbs each declare alive_pair.[sources].<verb>in your site config — either form, resolved by the same rules.- No flag, no entry in the file, and exactly one source declared for this verb — resolved. One candidate is not a choice, which is what makes a freshly installed kit work with no config file at all.
- Nothing declared anywhere, or several candidates with no default named — the command refuses and names both places a declaration can live. With several candidates it lists them; with nothing declared it names the config file it read and what the installed kits publish.
Every source you did not name yourself is announced, and where the announcement lands depends on the step that resolved it.
- From your site config (step 3). One line on stderr before the verb starts, naming the resolved spec, the
[sources]entry it was declared as, and the file it was read from. - From a kit's single declaration (step 4). No separate line; the receipt rides in the verb's own startup output.
newt restandnewt teleopfold it into their startup line, asbrought up by live_pair (from the benchkit kit)anddriving with live_pair (from the benchkit kit).newt recordandnewt record --teleopput it in thesourcerow of the preflight block, and under--jsonemit it as asourcekey alongsidecontract. - From
--source(steps 1 and 2). No announcement; you named the source yourself.
Site config location: $NT_SITE_CONFIG if set, else ~/.config/nt/nt.toml. newt reads [sources] out of that file and nothing else — the rest of it belongs to your kit.
[sources]
teleop = "live_pair"
record = "live_pair"
rest = "live_pair"
demonstration = "live_pair"Short names resolve against declarations only. newt does not scan for source modules and does not guess a name from a naming pattern, so a factory no installed kit declares is reachable by its full import path and no other way. For newt record, --simulate beats a configured default — a flag you typed is never overridden by a file.
Environment variables
| Variable | Scope | Behavior | Default |
|---|---|---|---|
NT_API_KEY | CLI + SDK | API key; takes precedence over ~/.nt/credentials | — |
NT_SITE_CONFIG | record, teleop, rest | Path to the rig's site config, read for its [sources] table | ~/.config/nt/nt.toml |
NT_CONSOLE_URL | login | Console URL for the pairing flow | https://newtheory-console.vercel.app |
NT_BOOTSTRAP_URL | status, SDK | Override the registry discovery base URL | https://nt-registry-production.up.railway.app |
NT_INFERENCE_URL | status, SDK | Override the inference endpoint; bypasses per-model routing via /v1/models | — |
NT_CONSOLE_URL | upgrade | Console URL the version check + newt upgrade read | https://newtheory-console.vercel.app |
NEWT_NO_UPDATE_CHECK | CLI | Set to 1 to disable the passive once-a-day "update available" notice | — |
Override warnings:
- CLI (
newt status): An amberOverrides active:block is printed whenNT_BOOTSTRAP_URLorNT_INFERENCE_URLis set, showing the active values. - SDK (
import newt):EnvOverrideWarningis emitted viawarnings.warn()whenNT_INFERENCE_URLis set. Warning text:NT_INFERENCE_URL is set to <url> — this overrides dynamic /v1/models discovery for ALL models. Unset it to use per-model cross-app routing.
Override variables are intended for development and testing against local or staging infrastructure. Unset them before running production workloads.
Related
- Getting started — install
newtand run your first inference - Authentication — credentials, key rotation, and team access