How it Works
The CLI never talks to Poly's servers directly. Every command connects to the desktop app over a local socket; the app owns the cache, the sync engine, and the server connection. That one design decision explains most of the CLI's behavior: reads are instant because they come from the local cache, writes work offline because they queue through the same sync engine the app uses, and there's no separate login because the app's session is the CLI's session.
The daemon
The desktop app doubles as a background daemon. When you run poly app start, the app launches hidden and serves the CLI (and integrations like MCP) over a local interface. If a command can't reach the app, it fails fast with a hint:
error: the Poly app is not running
→ start it with `poly app start`, or open the Poly app
poly app status, poly app restart, and poly app quit manage the daemon; see app.
File references
Every command that takes a file accepts four spellings, used interchangeably:
| Form | Example |
|---|---|
| Poly path | //Home/docs/report.pdf |
| Local path in the sync folder | ~/Library/CloudStorage/Poly/docs/report.pdf |
| Deep link | polyapp://file/id/abc123 |
| Web URL | https://poly.app/file/abc123 |
Poly paths start with //. A bare // means "all roots" — every account has //Home and //Archive, and each shared drive you belong to adds its own home and archive. Relative paths work too: when your working directory is inside the sync folder, . refers to the corresponding Poly folder, so poly ls with no arguments lists wherever you're standing.
Online, offline, and staleness
Commands serve from the local cache whenever they can:
- Reads — if the app is offline (or you pass
--cache-only), reads come from the cache. Cached data that the server hasn't re-confirmed is stale — usually fine, occasionally not. Pass--no-stalewhen a script must only act on server-confirmed data. - Writes — uploads and edits are queued as resumable jobs and survive disconnects and restarts. By default a write returns once it's durably queued;
--no-stalemakes it wait for server confirmation instead.
Two more flags matter when downstream steps depend on Poly's processing: --wait-for-indexing blocks until a written file is searchable, and --wait-for-transcoding waits for derived artifacts like video variants and PDF transcriptions (which can take a while).
Output for humans and for scripts
Every command has two output modes: pretty (the default — colored, tabular) and json:
poly ls //Home --output json | jq '.[].name'
The JSON is the same data the pretty renderer draws, so nothing is lost between modes. Errors follow the same rule — in JSON mode they're a single parseable object on stderr. Exit codes are script-friendly: 2 for usage errors, 69 when the app isn't running, and 75 for "not ready yet" conditions (like a timeout waiting for indexing) that are safe to retry.
Color respects --color, --no-color, and the ambient NO_COLOR convention. Most flags also read from environment variables (POLY_OUTPUT, POLY_TIMEOUT_MS, POLY_NO_STALE, …) — see Global Flags for the full reference.
Safety rails
Destructive commands (rm on archived files, rmdrive) prompt for confirmation, and --force skips the prompt. For environments that should never force anything --disallow-force (or POLY_DISALLOW_FORCE=1) globally refuses irreversible actions, and can only be switched on, never off, from inside the session.
Extending the CLI
An unrecognized subcommand isn't an error until Poly has checked your PATH: poly foo runs a poly-foo binary if one exists. This is the same mechanism git and cargo use for plugins. Feel free to use it for your own scripts, aliases, or custom skills and commands.