# Run one-off jobs from the CLI

> Run any command on a fresh Latchkey runner from your terminal or coding agent: the CLI packs your working tree, streams the logs back, and exits with the command's own exit code.

Source: https://latchkey.dev/documentation/latchkey-cli

## Summary

- `npm install -g @latchkeydev/cli`, then `latchkey run 'npm test'` runs the command on a fresh Latchkey runner and exits with the command's own exit code.
- Your working tree ships with the job: `.gitignore` rules apply, credential-shaped files are held back by default, and `.latchkeyignore` overrides both.
- Jobs bill as ordinary runner minutes from the same free-minute pool; their spend appears as its own **CLI Jobs** line on the Cost Analysis page.
- Needs an API key created with **Allow running CLI jobs (includes reading)**. Coding agents reach the same surface through the MCP `run_job` tool.

**The Latchkey CLI (`@latchkeydev/cli`) turns a fresh CI machine into something you invoke like a local command.** `latchkey run 'npm test'` packs your working tree, runs the command on an isolated Latchkey runner, streams the output to your terminal, and exits with the command's own exit code. That makes it a verification loop you can script: run the check the way CI will run it, on a clean Linux machine, before you push. It is built for pre-push verification, for reproducing failures that only happen in CI (clean workspace, fresh installs, Linux), and for coding agents, which drive it like any other shell command.

## Install and authenticate

1. **Install the CLI** Install globally with npm (Node 20.18.1 or newer), or run it ad hoc with `npx @latchkeydev/cli`. Install the scoped package name: the unscoped `latchkey` package on npm is an unrelated project.
2. **Create a jobs-enabled API key** In **Settings, API Keys** (owners and admins), generate a key with **Allow running CLI jobs (includes reading)** checked. Capabilities are fixed at creation: an existing key never gains them retroactively. The full key (it starts with `lk_live_`) is shown once.
3. **Log in** `latchkey login` validates the key without starting anything billable and saves it for this machine (`~/.config/latchkey/config.json`). In CI or an agent loop, skip the saved config and set the `LATCHKEY_TOKEN` environment variable instead.

## Run a job

```terminal
$ latchkey run 'npm test'
$ latchkey run --size large --timeout 3600 'npm ci && npm run build && npm test'
$ latchkey run --env NODE_ENV=test 'npm test'
```

Every run does the same five things: pack the current directory into a context archive, upload it, run the command on a fresh runner, stream the logs back live, and exit with the command's own exit code. A cancelled job exits 130 and a job the platform expired exits 124, so scripts and agents can branch on the outcome without parsing a line of log text.

> **The exit code is the verdict**
> `latchkey run 'npm test'` exiting 0 means the suite passed on a clean Linux runner; nonzero means it did not. Piping the output through another command replaces that verdict with the downstream command's status, so redirect to a file instead when you need both the logs and the code.

## The commands

| Command | What it does |
| --- | --- |
| `latchkey login` | Validate an API key and save it for this machine |
| `latchkey run` | Pack the current directory, run a command on a fresh runner, stream the logs, exit with the command's code |
| `latchkey list` | List recent jobs, newest first, including the ones still running |
| `latchkey logs` | Print a job's logs, or tail them to completion with `--follow` |
| `latchkey status` | Show a job's state, runner size, timing, and exit code |
| `latchkey cancel` | Request cancellation; a running job stops within about 10 seconds |
| `latchkey watch` | Poll for CI failures self-healing could not fix and hand each one to your coding agent |

The `run` flags you will actually use: `--size` picks the runner (`small` is the default; `medium`, `large`, and `xlarge` match the [runner sizes](/documentation/runners-overview) your workflows use), `--env KEY=VALUE` passes secrets and settings without shipping them as files, `--timeout` accepts 30 seconds to 2 hours (jobs default to 30 minutes), `--detach` submits the job and exits so you can tail it later, and `--no-context` runs the command in an empty workspace with nothing uploaded. Every remote command takes `--output json` for machine-readable output, and the package ships its own deep reference (`SKILL.md`) that a coding agent can read for the full contract.

## What ships with the job

`latchkey run` packages the directory you run it from, never the enclosing repository, so running from a subdirectory ships just that subdirectory. Three rules decide what goes in, and the last one to express an opinion wins:

- Every `.gitignore` inside the packaged tree applies, so ignored dependency installs and build output stay home and the job installs from scratch, the way CI does. Only ignore files inside the tree count: run from a subdirectory and the repository root `.gitignore` no longer applies (a `.latchkeyignore` at the packaging root is the fix).
- Files shaped like credentials (`.env` files, private keys, cloud credential files) are held back by a built-in deny-list, and every exclusion is printed. A `.gitignore` can never re-include them.
- A `.latchkeyignore` file (same syntax) outranks both: use it to exclude more, or to deliberately re-include something the deny-list held, which prints a loud warning naming the file.

> **Your git history stays home**
> The `.git` directory never ships and cannot be re-included, so the job sees your files, not your history or remotes. Need a commit SHA or branch name inside the job? Pass it with `--env`. Real secrets belong in `--env` too, never in the uploaded tree.

## Limits

| Limit | Value |
| --- | --- |
| Command line | 16,384 characters |
| Environment variables | 64 per job |
| Job timeout | 30 seconds to 2 hours, default 30 minutes |
| Context upload | 200 MB compressed, 1 GiB uncompressed, 250,000 entries |
| Job creation | 120 jobs per hour per workspace |
| Job records | Status and logs stay readable for about 24 hours after a job finishes; `latchkey list` still shows older jobs |

A context past its ceiling fails the pack locally, before anything is uploaded or billed, with the largest paths named so the fix is usually one `.latchkeyignore` line.

## Billing

CLI jobs are ordinary runner minutes: the same per-minute rate as a workflow job on the same size, rounded up per job, drawn first from the same monthly [free-minute pool](/documentation/runner-usage-and-free-minutes). The meter runs from when your command starts to when the job ends; queue and provisioning time is never billed. On the [Cost Analysis](/documentation/cost-analysis) page this spend appears as its own **CLI Jobs** line, because these jobs belong to no repository or workflow.

## Self-healing works here too

Every CLI job runs under the same [self-healing](/documentation/self-healing) machinery as your workflow jobs. Environment failures (a flaky registry, a missing system package, a full disk) are diagnosed in place while the job runs and fixed when a heal applies: a healed retry that passes exits 0, and when nothing applies the command's original exit code stands. Real defects in your code pass through unchanged so the exit code tells the truth. One difference from workflow jobs: a CLI job has no repository, so healing never opens a pull request; fixes happen on the runner or not at all. The diagnosis runs inside the billed window, so a red run bills noticeably more than a green one (typically about an extra minute); budget for it when iterating on a failure.

## The other direction: watch

`latchkey run` is you asking Latchkey to check something. `latchkey watch` is Latchkey telling you something broke: it polls the failures self-healing diagnosed but could not fix (your workflow runs, and CLI jobs too) and hands each new one to your coding agent (Claude Code by default, `--agent` for anything else), exactly once. The agent reads the full failure context through the [MCP server](/documentation/connect-your-ai-agent), so connect it there first. Use `--once` for a one-shot listing and `--no-spawn` to print without starting anything. Any key can run `watch`; it only reads.

## Works with

- [Connect your AI agent](/documentation/connect-your-ai-agent): the MCP `run_job` tool is the same job surface without a terminal. It starts from an empty workspace; the CLI is the way to ship your working tree.
- [Runners overview](/documentation/runners-overview): CLI jobs run on the same four runner sizes and the same [runner image](/documentation/runner-image-software), fresh for every job and destroyed at the end.
- [Runner usage and free minutes](/documentation/runner-usage-and-free-minutes): how the per-minute metering and the free tier work.
- [Cost Analysis](/documentation/cost-analysis): where CLI job spend appears in the dashboard.

## Common questions

### Does the job see my git history?

No. The `.git` directory is always excluded and cannot be re-included. The job sees your files as they are on disk; if the command needs a commit SHA or branch name, pass it through `--env`.

### Can a job reach my local services or databases?

No. The runner is a fresh, isolated machine with no route back to your laptop. It sees the uploaded tree and the environment variables you pass, nothing else. Interactive commands hang for the same reason: there is no terminal on the other end, so anything that prompts waits until the timeout ends the job.

### What happens when I press Ctrl-C?

It stops the tail, not the job: the job keeps running (and billing) on the runner. The interrupt notice prints the two follow-ups, `latchkey cancel <id>` to stop the job and `latchkey logs <id> --follow` to re-attach.

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
