{"id":13342,"plugin_id":"plugin_asdk_app_6a79c45722208191816dac268a720cb5","kind":"skill","collection_source":null,"comparison_source":null,"observed_at":"2026-09-30T23:07:19.827Z","digest":"8e2aff801b2eb006942477f8b4253e8e101a43676022d15ac9a6fe544ed635e7","against":null,"payload":{"name":"upstash-box-cli","description":"Drive an Upstash Box (a remote sandboxed workspace) from the terminal with the `box` CLI. Use when asked to run commands, edit files, clone repos, run builds or tests, publish a public URL, browse or screenshot a page, open a pull request or issue with a screenshot attached, schedule recurring work, run an AI agent, or do any work inside a box rather than on this machine.","included_files":[],"skill_md_contents":"---\nname: upstash-box-cli\ndescription: Drive an Upstash Box (a remote sandboxed workspace) from the terminal with the `box` CLI. Use when asked to run commands, edit files, clone repos, run builds or tests, publish a public URL, browse or screenshot a page, open a pull request or issue with a screenshot attached, schedule recurring work, run an AI agent, or do any work inside a box rather than on this machine.\n---\n\n`box` operates on a **remote container**, not this machine. Your own file and shell\ntools act locally; anything that must happen inside the box goes through `box`.\n\n## Install\n\n```bash\nnpm i -g @upstash/box-cli\n```\n\n## Authentication\n\nEvery command needs an API key, or it fails with \"API token required\". Set it once,\nor pass `--token` on any single command. Create one at\nhttps://console.upstash.com/box.\n\n```bash\nexport UPSTASH_BOX_API_KEY=box_...\n```\n\n## Selecting a box\n\nResolution order is `--box <id>`, then `$BOX_ID`, then the nearest `.box` file\n(searched upward). Create one and pin it to the working directory:\n\n```bash\nbox create --no-repl --runtime node                    # prints the id, writes .box\nbox create --no-repl --runtime node --clone-repo https://github.com/org/repo\nbox list                                               # find an existing box\nbox use <box-id>                                       # pin one to this directory\nbox status                                             # id, where it came from, state\n```\n\n`--keep-alive`, `--browser`, `--env` and `--size` can only be chosen at create\ntime; to change any of them you make a new box. There is no resize.\n\nDefault to a plain `box create --no-repl`. A plain box pauses when it goes idle\nand resumes on the next command, which is what almost all work wants:\n\n```bash\nbox create --no-repl --browser             # provision a headless Chromium\nbox create --no-repl --env KEY=VAL         # env for this box (repeatable)\nbox create --no-repl --size medium         # small (default), medium, large\n```\n\nAdd `--keep-alive` only when something has to survive an idle gap: a detached\nserver you are about to reach over a preview URL, or a job that keeps running\nbetween commands. It stops the box pausing, so the box keeps costing money until\nyou pause or delete it. `--init-command` is rejected without it:\n\n```bash\nbox create --no-repl --keep-alive                            # stays up when idle\nbox create --no-repl --keep-alive --init-command \"npm ci\"    # startup script\n```\n\nThe rest of the create-time options, all equally unchangeable afterwards:\n\n```bash\nbox create --no-repl --skill upstash/skills/redis      # repeatable\nbox create --no-repl --mcp docs=@org/mcp-server        # or name=https://url\nbox create --no-repl --mcp-file servers.json           # for args and headers\nbox create --no-repl --network-policy deny-all         # or allow-all, or custom\nbox create --no-repl --network-policy custom --allow-domain api.example.com\nbox create --no-repl --attach-headers-file headers.json\n```\n\n`--attach-headers-file` holds a JSON object keyed by host pattern\n(`{\"api.stripe.com\": {\"Authorization\": \"Bearer ...\"}}`), and those headers are\ninjected into matching outbound requests from the box. There is an\n`--attach-header host:Name=value` form too, but the value lands in `ps` and in\nshell history, so prefer the file for anything secret.\n\nA skill id has three parts, `owner/repo/skill-name`. A malformed one is only\nwarned about server-side, so the box comes up with the skill silently absent.\n\n`--env` is per-box. `box env set` is account-level: it is merged into every box\ncreated afterwards, never into one that already exists. A per-box `--env` wins\nfor the same key, so account-level values only fill in what the box did not set.\nAccount-level skills and MCP servers are merged the same way.\n\n`paused` is not an error; the next command resumes the box.\n\nA `.box` file is found by walking **up** from the working directory, so `cd`-ing\ninto another project can silently pick up a pin left there earlier and run\nagainst the wrong box. In any session touching more than one box, pass `--box`\nexplicitly; `box status` says which box it resolved and where that came from.\n\nClean up when the work is done. Boxes cost money while they exist:\n\n```bash\nbox pause                   # keeps the workspace, resumes on the next command\nbox resume                  # rarely needed; any command resumes a paused box\nbox delete --yes            # irreversible; --yes is required without a terminal\n```\n\nNever run `box create` or `box connect` without `--no-repl`: they open an\ninteractive REPL and will hang. `box from-snapshot` takes `--no-repl` too.\n\n```bash\nbox snapshot                                # snapshot this box, prints the id\nbox snapshot list\nbox from-snapshot <snapshot-id> --no-repl   # restore into a new box, pinned\nbox snapshot delete <snapshot-id>\n```\n\n## Running commands\n\nPut the remote command after `--`, or its flags are parsed as `box`'s own.\n\n```bash\nbox exec -- npm install\nbox exec -C repo -- npm test\nbox exec --json -- node -e 'console.log(1)'   # {stdout, stderr, exit_code}\n```\n\nThe remote shell is `sh`, not bash. A heredoc inside `box exec` fails with\n`Syntax error: redirection unexpected`; write the file with `box files write - `\ninstead, or wrap the command in `bash -c` when the box has bash.\n\nFor an interactive shell, ssh straight in. The box id is the user and the Box\nAPI key is the password:\n\n```bash\nssh <box-id>@us-east-1.box.upstash.com\n```\n\nCommands run as `boxuser`, so a global npm install needs sudo, which is\npasswordless:\n\n```bash\nbox exec -- 'sudo npm install -g @upstash/docs7'   # EACCES without sudo\n```\n\nOne argument is a shell expression, sent as written, so pipes and redirection work.\nSeveral arguments are argv and are quoted individually, so an argument containing\nspaces stays one argument.\n\nThe remote command's exit code is passed through, so `box exec -- npm test && ...`\nchains normally. Exit code **125** means the CLI itself failed (bad box, bad flags),\nnever a status the remote command returned.\n\nA background server dies with the command that started it. Detach it:\n\n```bash\nbox exec -- '( npm run dev > dev.log 2>&1 & )'\nbox public-url 3000                               # prints the public URL\nbox public-url list\nbox public-url delete 3000\n```\n\nInline code, when a shell one-liner would be worse than a program:\n\n```bash\nbox code - --lang python < script.py\nbox code 'console.log(1 + 1)' --lang js\n```\n\n## Building something and handing back a link\n\n\"Make me a snake game, use Upstash Box\" is a request to build it in a box, run it\nthere, and reply with a URL the user can open. Do the whole thing; do not stop at\nwriting the file.\n\n```bash\nbox create --no-repl --runtime node --keep-alive   # writes .box; stays up for the URL\n\nbox files write index.html - <<'HTML'\n<!doctype html><meta charset=\"utf-8\"><title>Snake</title>\n<canvas id=\"c\" width=\"400\" height=\"400\"></canvas>\n<script>/* the game */</script>\nHTML\n\nbox files write server.js - <<'JS'\nconst http = require(\"http\"), fs = require(\"fs\");\nhttp.createServer((_, res) => {\n  res.writeHead(200, { \"Content-Type\": \"text/html\" });\n  res.end(fs.readFileSync(\"index.html\"));\n}).listen(3000, \"0.0.0.0\");   // the default binds ::, which the check below misses\nJS\n\nbox exec -- '( node server.js > server.log 2>&1 & )'          # detached, or it dies\nbox exec -- 'sleep 1; ss -ltn | grep -q \"0.0.0.0:3000\" && echo up'\nbox public-url 3000                                           # the link to reply with\n```\n\nNode's own `http` module rather than a package: no install, no network fetch, and it\nworks on a bare `node` runtime.\n\nCheck the port before publishing it, and check what it is **bound to**, not just\nthat it answers. A server on `127.0.0.1` replies to a curl from inside the box\nand still cannot be published: the proxy reaches the container by address, so\n`box public-url` returns 502. That is why the check above greps for `0.0.0.0`\nrather than curling localhost, which passes in exactly the case that fails.\n\nMost dev servers need telling: `--host 0.0.0.0` for Vite and many others,\n`-H 0.0.0.0` for some, and a few cannot be moved off loopback at all.\n\n`--keep-alive` is what keeps the link working. Without it the box pauses when\nidle, the detached server dies with it, and the URL you handed over starts\nanswering errors some minutes later.\n\nReply with the URL itself, not just \"it is running\". Say that the box keeps costing\nmoney until `box delete --yes`, and that the URL is public to anyone who has it —\n`box public-url 3000 --basic-auth` puts credentials in front of it.\n\n## Files\n\nPaths are relative to `/workspace/home`.\n\n```bash\nbox files list src\nbox files read src/index.ts\nbox files write src/app.ts -    < local.ts    # - reads stdin: use this for code\nbox files write notes.txt \"short text\"\nbox files stat src/index.ts\nbox files mkdir -p a/b/c\nbox files rename old.ts new.ts\nbox files remove build -r                      # a directory needs -r\nbox files upload ./local.zip /workspace/home/local.zip\nbox files download repo                        # a folder lands in ./repo\nbox files download logs/app.log -o ./app.log   # a file; -o names the destination\n```\n\nWrite code with `-` and stdin. Passing source as an argument mangles it in the shell.\n\nTo search, use the box's own tools: `box exec -- grep -rn TODO src`.\n\n## Git\n\nA clone lands in a directory named after the repo, and every git verb except `clone`\nneeds that directory via `-C`. Without it git runs at the workspace root, which is not\na repository.\n\n```bash\nbox git clone https://github.com/org/repo\nbox git clone https://github.com/org/repo -C my-app   # -C is the destination here\nbox git status -C repo\nbox git diff -C repo\nbox git config -C repo --name \"Bot\" --email bot@example.com\nbox git checkout -C repo feature/x             # creates the branch if missing\nbox git exec -C repo -- add -A\nbox git commit -C repo -m \"message\"\nbox git push -C repo                           # pushes the checked-out branch\nbox git create-pr -C repo --title \"Fix the thing\" --base main\nbox git create-pr -C repo --title \"Fix the thing\" --body-file notes.md\nbox git create-issue -C repo --title \"Search returns nothing\"\n```\n\nUse `--body-file` for anything longer than a sentence: a body worth writing does\nnot survive shell quoting. `-` reads stdin.\n\n`box git exec` takes git's arguments without the leading `git`, and passes git's exit\ncode through.\n\nPrivate repos and PRs need a token at creation: `box create --no-repl --git-token $GITHUB_TOKEN`.\n\n## Attaching a screenshot to a pull request or issue\n\n`--attach` uploads an image or video to the new pull request or issue, and repeats\nfor several. Alt text for an image goes after a `#`. A video renders as a player and\ntakes no alt text.\n\nThe path is read inside the box, relative to `-C`. A browser screenshot is written to\nthe machine running the CLI, not into the box, so it has to be uploaded first. That\nupload is the step people miss:\n\n```bash\nbox browser screenshot -o /tmp/shot.png          # lands here, not in the box\nbox files upload /tmp/shot.png repo/shot.png     # now it is in the repository\nbox git create-issue -C repo \\\n  --title \"Search returns nothing\" \\\n  --body 'Reproduced on staging.\n\n![what I saw](./shot.png)' \\\n  --attach 'shot.png#the empty result list'\n```\n\nA `![alt](./shot.png)` reference in the body is rewritten to point at the uploaded\nasset, so the image renders in the issue instead of pointing at a path that exists\nonly inside the box.\n\nFour rules are enforced, each a 400 before anything is created: the extension must be\npng, jpg, jpeg, gif, webp, mp4, mov or webm; at most 50 files; the path must stay\ninside the `-C` directory; and a video cannot carry alt text.\n\nWhen some attachments upload and others fail, the item is still created and its URL\nis still returned, with a `warning` alongside it. Text output prints the warning on\nits own line, and `--json` carries it as the `warning` field. Check it before\nreporting the issue as filed with its evidence attached.\n\n## Agent\n\nIf the box was created with an agent, hand it a task:\n\n```bash\nbox create --no-repl --agent-harness claude-code --agent-model anthropic/claude-sonnet-5\nbox run \"Fix the failing test in src/auth.test.ts\"\nbox run - < prompt.txt\n```\n\nText goes to stdout, tool calls to stderr. Prefer doing the work yourself with the\ncommands above; `box run` is for delegating a whole task to the box's own agent.\n\n## Watching and stopping work\n\n```bash\nbox status runs                    # id, type, status, duration, cost\nbox status logs --limit 50\nbox cancel <run-id>                # ids come from status runs\n```\n\nA run started by another process cannot be stopped any other way: `box cancel`\ntakes the id, so a long agent run or build is interruptible from a fresh shell.\n\n## Browser\n\nOnly on a box created with `--browser`. Chromium **runs inside the box**, so it\nreaches your app on `http://localhost:3000` with no public URL involved. What\nlives outside is only the control path: these commands reach Chromium through\nthe API, so there is no `box exec` spelling of them. A script running in the box\ncan still talk to Chromium directly over CDP, which is the escape hatch at the\nend of this section.\n\n```bash\nbox browser open https://example.com    # prints the tab id\nbox browser tabs\nbox browser content                     # title, url, text, links\nbox browser screenshot -o page.png\nbox browser goto https://example.com/login\nbox browser act \"click the login button\"\nbox browser close\nbox browser cdp-url                     # drive it with Playwright instead\nbox browser observe \"what can I click here?\"\nbox browser live-url                    # a URL for a human to watch the tab\n```\n\nEvery `box browser act` is metered: it takes an instruction in words and needs\na model to read the page. The SDK can replay an `observe()` result for free,\nbut the CLI takes only the string form, so a loop of `act` calls costs a model\ncall each time. `content`, `goto`, `screenshot` and `close` are not metered.\n\nRecordings, when you need to show what happened rather than describe it:\n\n```bash\nbox browser recordings start --max-seconds 120\nbox browser recordings stop\nbox browser recordings list\nbox browser recordings get <recording-id>\nbox browser recordings download <recording-id> -o session.mp4\n```\n\nChromium starts on first use, so the very first `box browser open` is slower\nthan the rest, and anything talking to CDP directly fails until it has run once.\n\n`--tab <id>` is optional while one tab is open and required once there are\nseveral. `screenshot` writes to a file because stdout carries text, and that file\nlands on this machine rather than in the box. To put a screenshot on a pull request\nor issue, see \"Attaching a screenshot to a pull request or issue\".\n\nPull structured data off the page with a flat JSON Schema file:\n\n```bash\necho '{\"type\":\"object\",\"properties\":{\"price\":{\"type\":\"string\"}},\"required\":[\"price\"]}' > s.json\nbox browser extract \"the listed price\" --schema s.json\n```\n\nA property not named in `required` is optional. Nested objects are refused.\n\n### Capturing straight into the box\n\n`box browser screenshot --full-page -o page.png` is the short way, and it is\nenough whenever the image can live on this machine. It writes to the machine\nrunning the CLI, though, so getting the image into the box costs an upload.\n\nChromium's CDP is open on `127.0.0.1:9222` **from inside the box** with no\ntoken, so a script running there captures and writes in one step, and can clip\nto a single element, which the CLI does not expose:\n\nWrite the script with `box files write` rather than inlining it: the remote\nshell is `sh`, and quoting a program through `box exec` is where this goes\nwrong.\n\n```bash\nbox files write shot.mjs - <<'JS'\nconst targets = await (await fetch(\"http://127.0.0.1:9222/json\")).json();\nconst page = targets.find((t) => t.type === \"page\");\nconst ws = new WebSocket(page.webSocketDebuggerUrl);\nawait new Promise((r) => (ws.onopen = r));\n\nlet id = 0;\nconst pending = new Map();\nws.onmessage = (m) => {\n  const msg = JSON.parse(m.data);\n  pending.get(msg.id)?.(msg.result);\n  pending.delete(msg.id);\n};\nconst send = (method, params = {}) =>\n  new Promise((resolve) => {\n    const callId = ++id;\n    pending.set(callId, resolve);\n    ws.send(JSON.stringify({ id: callId, method, params }));\n  });\n\n// captureBeyondViewport only permits capture outside the viewport; the clip is\n// what makes it the whole page. cssContentSize is in CSS pixels, which is what\n// clip expects.\nconst metrics = await send(\"Page.getLayoutMetrics\");\nconst size = metrics.cssContentSize ?? metrics.contentSize;\nconst { data } = await send(\"Page.captureScreenshot\", {\n  format: \"png\",\n  captureBeyondViewport: true,\n  clip: { x: 0, y: 0, width: size.width, height: size.height, scale: 1 },\n});\n\nconst fs = await import(\"node:fs\");\nfs.writeFileSync(\"shot.png\", Buffer.from(data, \"base64\"));\nws.close();\nJS\n\nbox exec -- 'node shot.mjs'      # shot.png is now in the box\n```\n\nThe `clip` is what makes this a full-page capture rather than a viewport one;\n`--full-page` does the same thing. An element-clipped capture is the same call\nwith that element's box as the clip, and that one has no CLI flag. Node's global\n`fetch` and `WebSocket` are enough, so nothing has to be installed, but Chromium\nmust have been started once by a `box browser` command first.\n\nThis is only worth it when the image should stay in the box or you need a\ncapture the CLI cannot make. Otherwise `screenshot -o` then `files upload` is\nshorter.\n\n## Schedules\n\nCron on the box, in UTC. Nothing inside the container can register one.\n\n```bash\nbox schedule exec --cron '0 9 * * *' -- npm run backup\nbox schedule agent --cron '@daily' \"summarise yesterday's errors\"\nbox schedule list\nbox schedule get <schedule-id>            # includes run and failure counts\nbox schedule pause <schedule-id>\nbox schedule resume <schedule-id>\nbox schedule update <schedule-id> --cron '0 10 * * *'\nbox schedule delete <schedule-id>\n```\n\n`update` changes only what you name, so setting the cron leaves the command alone.\n\n## Box configuration\n\n```bash\nbox skills add upstash-redis-js        # skills available to the box's agent\nbox skills list\nbox skills remove upstash-redis-js\nbox config model anthropic/claude-sonnet-5\nbox config init-command set \"npm ci\"   # keep-alive boxes only; runs on start\nbox config init-command get\nbox config init-command delete\nbox config network deny-all            # or allow-all, or custom\nbox config network custom --allow-domain api.example.com\nbox config harness --command my-agent  # a custom agent harness\n```\n\nAccount-level settings, which apply to boxes you create later rather than to\nthis one:\n\n```bash\nbox env set KEY VAL                    # applies to boxes created after this\nbox env list\nbox env delete KEY\nbox env set-all A=1 B=2                # replaces every var, does not merge\nbox labels add staging                 # then: box list --label staging\nbox labels list\nbox labels remove staging\n```\n\nBoth `box env set` and `box create --env` take the value as an argument, so a\nsecret passed either way is visible in `ps` and lands in shell history. Neither\nis a secrets mechanism; keep real credentials out of both and use a token the\nbox fetches for itself.\n\n## Flag reference\n\nThe flags the walkthroughs above do not reach. Every command also takes the\nglobal `--box`, `--json` and `--token`.\n\n```bash\nbox create --no-repl --git-user-name N --git-user-email E   # commit identity\nbox create --no-repl --agent-api-key stored                 # key saved in the console\nbox create --no-repl --no-use                               # do not write .box\nbox init-demo --directory my-demo                           # scaffold elsewhere\n\nbox exec -C /srv/app -- npm test         # -C/--cwd: working directory\nbox run --timeout 600 -q \"...\"           # -q/--quiet: no tool-call logs on stderr\nbox code --timeout 120 \"...\"             # both take --timeout in seconds\n\nbox files read --offset 0 --length 65536 big.log   # a slice; 8 MiB per read\nbox files read --encoding base64 logo.png          # binary out\nbox files write --encoding base64 logo.png -       # binary in\nbox files remove -r build/                         # -r required for a directory\nbox files mkdir -p a/b/c                           # -p creates missing parents\nbox files stat --follow link                       # resolve a final symlink\n\nbox git clone --branch main --depth 1 <url>        # shallow, single branch\nbox git clone --github-token $TOKEN <url>          # private repository\nbox git commit -m \"msg\" --author-name N --author-email E\nbox git push --branch feature/x                    # names the branch to push\n\nbox public-url 3000 --bearer-token       # or --basic-auth; both generate credentials\nbox use --unset                          # drop this directory's .box, never a parent's\n\nbox schedule agent --cron \"0 9 * * *\" --model <m> --timeout 300 --webhook-url <url> \"...\"\nbox schedule update <id> --timeout 0     # 0 clears the timeout; --prompt, --cron, --model too\n\nbox config network custom --allow-domain a.test --allow-cidr 10.0.0.0/8 --deny-cidr 10.1.0.0/16\nbox config harness --command my-agent --arg --verbose   # --arg repeatable, sent before the prompt\n```\n\n`-C` means the working directory on `box exec` (`--cwd`) and the repository\ndirectory on every `box git` and `box schedule` subcommand (`--folder`).\n\n## Output\n\nData goes to stdout, diagnostics to stderr, so piping is safe. `--json` prints the\nresult as JSON with no wrapper, on every command that returns data. The ones that\nopen a REPL or print a shell script (`connect`, `init-demo`, `completion`)\nreject it rather than answering an automation caller with a prompt:\n\n```bash\nbox files list --json | jq -r '.[].name'\nbox get \"$(cat .box)\" --json\n```\n\n`box init-demo` and `box completion` exist but are for people, not agents: one\nscaffolds a local demo project, the other prints a shell completion script.\n"},"changes":[],"summary":"First saved snapshot. No earlier version is available for comparison.","summary_kind":"deterministic","summary_metadata":{}}