Documentation menu
Getting started
Dashboard & analytics
- Dashboard at a glance
- Cost analysis
- Pipeline performance
- Optimization insights
- Knowledge Base
- Connect your AI agent
Managed runners
- Runners overview
- Run your first job
- Migrate from GitHub-hosted
- Latchkey CLI
- The Runners page
- Custom runners (AI Scan)
- Self-healing
- Runner image & software
- Provisioning & warm pools
- Limits & concurrency
Caching
Team & notifications
Billing & plans
Help
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.
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#
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.
$ npm install -g @latchkeydev/cli
$ latchkey --helpCreate 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.
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#
$ 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 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 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
.gitignoreinside 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.gitignoreno longer applies (a.latchkeyignoreat the packaging root is the fix). - Files shaped like credentials (
.envfiles, private keys, cloud credential files) are held back by a built-in deny-list, and every exclusion is printed. A.gitignorecan never re-include them. - A
.latchkeyignorefile (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.
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. The meter runs from when your command starts to when the job ends; queue and provisioning time is never billed. On the 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 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, 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: the MCP
run_jobtool 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: CLI jobs run on the same four runner sizes and the same runner image, fresh for every job and destroyed at the end.
- Runner usage and free minutes: how the per-minute metering and the free tier work.
- 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.