# cowork.json — the app manifest (full reference)

A folder `.cowork/apps/<name>/` containing a `cowork.json` is a Cowork app. Apps are discovered
ONE level deep under `.cowork/apps/`, in folder-name order. A folder counts only when its own name is
lowercase kebab (`^[a-z0-9][a-z0-9-]{0,62}$`, not `apps`/`data`/`courses`/`local`, not a Windows device
name such as `con` or `nul`) — dotted, uppercase, symlinked and dot-prefixed folders are skipped. A Keep-both
copy of an update sits beside yours as `<name>-v1-2-3`. Every path inside the manifest is RELATIVE TO THE APP FOLDER, uses forward
slashes, and may not escape it (no leading `/`, no `..`, no `\`, no drive letters).

The IDE validates `.cowork/apps/*/cowork.json` against the manifest schema as you type — no
`$schema` key is needed (unknown keys, `$schema` included, are ignored).

## Keys

| key | type | rules |
|---|---|---|
| `name` | string, **required** | `^[a-z0-9][a-z0-9-]{0,62}$`. Store id AND activity-bar id. Unique per workspace (a second folder claiming it is reported broken) and per team-server store. Never rename a published app — a new name is a new app. |
| `version` | string, **required** | semver `X.Y.Z` (optional `-pre` / `+build`), no leading `v`. |
| `title` | string ≤ 200 | Display name (rail tooltip, pane title, store card). Default: `name`. |
| `description` | string ≤ 2000 | One or two sentences for the store card. |
| `icon` | string | A codicon id (`mail`, `calendar`, `rocket`, `graph`, `database`, `terminal` … — https://microsoft.github.io/vscode-codicons/) or `$(mail)`, **or** an image file inside the app folder (`icon.png`, `assets/logo.svg`; `.png`/`.svg`/`.webp`/`.jpg`, ≤ 256 KB, not a symlink). An image fills the rail tile in full color (centered, never cropped; `color` shows behind transparent parts) — square art with its own padding looks best. Manage Workspace and the Team Servers store show it too (`cowork push` puts it on the store listing; a codicon clears an old image there). Never a URL. Absent → a letter tile. |
| `color` | string | Hex tile color `#rgb` / `#rrggbb` (e.g. `#EA4335`); the glyph turns white. |
| `entry` | string | The UI, `.jsx` or `.tsx`. Present → the app gets its own activity-bar icon. Absent → a routines-/routes-only app (no icon). |
| `web` | `{ "index": "<.html file>", "open"?: "drop:<css>", "download"?: { "url", "sha256" } }` | Makes the folder a **web app** instead of a JSX one — see "Web apps" below. Never together with `entry` (the `entry` wins, `web` is dropped with an error). |
| `display` | `sidebar` \| `tab` \| `modal` | What clicking the icon does with `entry`. Default `sidebar`; a web app is always `tab` (another value is dropped with an error). |
| `open` | string \| `{ "routine": id, "args"?: {…} }` | **`sidebar` only.** What to open in the EDITOR the first time the icon is clicked in a session: an app file, or a routine that returns `{ "path": "<workspace-relative file>" }` (for content that must be fetched/materialized first). Ignored (with an error) for `tab`/`modal`. |
| `routines` | `{ id: file }` or `{ id: { "file": file, "timeout"?: s, "readOnly"?, "destructive"?, "reachesOthers"?, "description"?, "input"? } }` | id `^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$` → `.ts` / `.js` / `.mjs` file. ≤ 100. `timeout` (optional): how long one run may take, whole seconds 1–300 (default 30). The rest says what the routine is as a **tool** — what an agent sees and is granted ([agents and grants](/docs/guides/permissions.md)): `readOnly: true` it changes nothing (on for every agent by default; never together with the next two), `destructive: true` what it changes cannot be undone, `reachesOthers: true` it reaches people beyond the account (sends, posts, pays); `description` one line (≤ 400 chars); `input` its arguments as a JSON Schema `{"type": "object", "properties": {…}, "required": […]}` (≤ 8 KB). A bad tool key is reported and left at its safe default (a write, no description); the routine stays. A bare `"id": "file.ts"` is a write with no description. A `permission` key is ignored (reported). |
| `fileRouting` | `{ glob: appFile }` | A WORKSPACE-relative glob (`**/*.maillist.json`) → an app file in this folder that renders matching files. ≤ 50. A glob ending in `/` routes FOLDERS (`"Notebook/"`, `"Projects/*/"`): clicking the folder in the Explorer opens the app, and a folder route never matches a file (nor a file route a folder); its value must be a `.jsx`/`.tsx`. An `.html` value opens the file in the app's web app, so it must be its `web.index` (any other `.html` value is dropped with an error). |
| `credential` | `{ "oauth"?: { "provider", "scopes", "ownClient"? }, "key"?: { "fields", "url"?, "hint"? } }` | **A capability's** connection to its service — a sign-in, a pasted key, or either (one at least). `oauth`: `provider` an OAuth preset (`google`, `microsoft`), the `scopes` its routines need (≤ 32); every capability declaring a provider shares ONE sign-in, named after it (`google`), and connecting asks for what they all need together; `ownClient: true` signs in through the user's own client instead, under the capability's own name. `key`: the `fields` the person pastes (≤ 8, lowercase letters and `_`, e.g. `["token"]`), stored as `<capability>.<field>`; `url` (https) where to get them, `hint` (≤ 200 chars) one line on how. The person connects once — Manage Workspace › Components › Credentials, an app's pane (`cowork.credentials.connect`), or `cowork credential connect`; routines use `ctx.credential.*`. An app declares none (ignored, with a problem line); the old `accounts` / `secrets` keys are ignored (reported). |
| `capture` | `["screen" \| "system-audio" \| "mic" \| "screenshot", …]` | What the app's panes may capture with `cowork.capture` (any subset; an unknown entry drops the key with a problem line). A recording with video needs `screen`, its sound `system-audio` and/or `mic`; `capture.shot` needs `screenshot`. Declaring is not consent: every capture still asks the person in Cowork's dialog. Manage Workspace › the app's page lists it. An app's only (a capability has no panes). |
| `kind` | `app` (default) \| `capability` | A `capability` is a package of routines with no UI that apps use — and agents, through Cowork's MCP door, as granted — a Gmail capability is the `gmail-*` routines + its `SKILL.md`. It carries no `entry` or `fileRouting` and no `uses` of its own (reported if it does; `kind` never changes what the folder registers). Manage Workspace lists it under Components › Capabilities instead of Apps. Credentials belong to a capability: its routines reach its own connection (`credential`) and `<name>.*` secrets; an app's reach none (`NotGranted`). A package that declares neither `kind` nor `uses` predates capabilities and keeps its old access until it declares one. |
| `uses` | `{ "<capability name>": "<X.Y.Z>" }` | The capabilities an app needs, each with the lowest version it works with; met by a switched-on package of that name at the same major and that version or above. ≤ 8, never the app's own name. A template install of an app brings the capabilities it `uses` along from the same team server; a missing, off or too-old one is listed in Components › Capabilities, and a routine call it would have answered fails with an error naming it. Declaring `uses` puts an app under the capability model (as `"kind": "app"` does): its widgets and routines reach another package's routines only through a capability named here (`Forbidden` otherwise, naming the line to add). |
| `requires` | `[]` \| `["web"]` \| `["desktop"]` | Where it can run. `["desktop"]` = the Cowork Studio desktop app only (e.g. it uses `ctx.browser`). Empty = anywhere. |
| `tags` | string[] ≤ 20, each ≤ 40 chars | Store search/filters. |
| `banner` | string | The store listing's wide picture: an image file in the app folder (`.png`/`.svg`/`.webp`/`.jpg`, ≤ 1 MB, not a symlink). `cowork push` uploads it when it changed. |
| `statusBar` | `{ "routine": id, "args"?: {…}, "interval"?: s }` | One item in the footer (status bar), drawn from what `routine` — one of THIS app's routines — returns. `interval`: seconds between refreshes while the window has focus, 15–3600, default 60. See "Footer item" below. |
| `screenshots` | string[] ≤ 5 | The store listing's screenshots, in order — image files in the app folder (≤ 1 MB each). `cowork push` uploads the changed ones and removes any the listing holds beyond them; leave the key out to manage screenshots in the Team Servers store instead. |

Validation: an invalid `name` or `version` makes the folder "not an app" (shown as broken,
never registered). Any other bad key is dropped with an error and the rest of the app keeps
working. Unknown keys are ignored.

## Display modes

- **`sidebar`** — `entry` renders in a side panel under the app's icon. Optional `open` puts
  something in the editor area too (once per session, so the user isn't yanked around).
- **`tab`** — clicking the icon opens `entry` in an editor tab (no side panel), every click.
- **`modal`** — clicking opens `entry` in a resizable in-workbench dialog.

## Web apps

A web app is a prebuilt static site — an `index.html` with its scripts, styles, `.wasm` and
fonts, e.g. a Rust/WASM editor's release build — that Cowork serves **unmodified** on its own
origin and draws in an editor tab of the normal editor groups (no URL bar, no browser history):

```json
{ "name": "wordcraft", "version": "0.10.0", "title": "WordCraft", "icon": "icon.png",
  "web": { "index": "web/index.html" },
  "fileRouting": { "**/*.docx": "web/index.html" } }
```

- `web.index` is the page, inside the folder; the folder that holds it is what gets served
  (dotfiles and anything outside it never are). Unzip the site's release into `web/` as it ships.
- Or keep the build out of the app: `"download": {"url": "https://…/site.zip", "sha256": "<hex>"}`
  fetches the pinned release on the first open (checked against the sha256 before it is unpacked,
  cached per version). `index` is then relative to the BUILD — a zip whose files sit in one top
  folder has it stripped — so it is usually `"index.html"`, and routes name `"index.html"` too. A
  build copied into the folder as `web/<index>` still wins.
- `display` is always `tab`; there is no `entry`, no widget and no `window.cowork` SDK — the page
  is the app. Routines and `SKILL.md` work as in any app.
- A routed file (`**/*.docx` above) opens in the web app's tab. Open the app directly with
  `cowork app open .cowork/apps/<name>/web/index.html`.
- The app gets the routed file the web's standard ways — its open picker, a file `<input>`,
  `launchQueue`; saving under the file's own name writes it back, any other name asks where.
  An app that opens files only by a drop sets `"open": "drop:<css selector>"` (e.g.
  `"drop:#canvas"`): the file is dropped on that element.
- It opens in the Cowork Studio desktop app and in Cowork in a browser tab or the PWA alike: the
  daemon serves it on its own `http://<name>-<hash>.app.localhost:<port>/` origin. Like a JSX app it
  gets an activity-bar icon. In a browser tab, closing it with unsaved changes does not ask yet.

## Site apps

A site app is a **web site installed as an app**: `web.whatsapp.com`, a dashboard, any https page. It
draws in an editor tab (no address bar) on your signed-in browser accounts, so the site is already
logged in, and its notifications and unread count become the app's. It is a normal workspace app, a
folder `.cowork/apps/<name>/` with a `cowork.json`, so it can be pushed like any other:

```json
{ "name": "whatsapp", "version": "1.0.0", "title": "WhatsApp", "icon": "icon.png",
  "web": { "url": "https://web.whatsapp.com/", "scope": "https://web.whatsapp.com/" } }
```

- `web.url` is an https page (http only on localhost) with no credentials in it. `scope` is an https
  prefix on the same origin; it defaults to `url` up to its last `/`. A manifest has `web.index` (a
  web app) or `web.url` (a site app), not both, and `fileRouting` cannot point at a site app.
- **Install** from the in-IDE browser: the address bar shows an install icon when the page has a Web
  App Manifest, and the menu offers "Install \<name>…" (or "Install page as app…" for any https page).
  Or run `cowork app install <https url>`: the name, icon and color come from the site's own web app
  manifest (the page's title and icon when it has none). `cowork app remove <name>` deletes the files the
  install wrote; if cowork.json no longer points at the installed url, or a file changed, it keeps it and says so. Open one with
  `cowork app open .cowork/apps/<name>/cowork.json`.
- **In the right side bar**: `"display": "right"` puts the site in a tab of its own in the right side bar,
  beside Chat and Claude Code — ChatGPT, Claude, Gemini next to your files. `"display": "sidebar"` puts it in
  a pane of the left side bar, opened from its activity-bar icon; with neither it opens in an editor tab.
  `cowork app install https://chatgpt.com/ --title ChatGPT --display right` installs it there (chatgpt.com
  and claude.ai turn scripts away, so the CLI installs them from the address with no icon — `--title` names
  them); for an installed one, set the key in its cowork.json. The page keeps running while its pane is hidden (another tab of the side bar, the
  side bar closed), so a long reply keeps streaming. In the right side bar any title works (ChatGPT, whose
  name starts with Chat, gets no rail icon as a tab app). A JSX app can be `"display": "right"` too: its
  sidebar UI then lives in the right side bar (`open` is a sidebar app's only — it runs from a rail click).

```json
{ "name": "chatgpt", "version": "1.0.0", "title": "ChatGPT", "display": "right",
  "web": { "url": "https://chatgpt.com/" } }
```
- **Notifications** the site raises reach Cowork like an app's: a banner (muted per app), the bell,
  routines started with `cowork deploy add <id> --notifications <app>`, and `cowork.onNotification`.
  The site's unread count (`navigator.setAppBadge`) shows on the dock or taskbar icon. An OS banner
  click goes to the chat or page; the bell's Open shows the app but does not click for you.
- Agents can look at and use a site app's tab (`apps list|screenshot|click|type|press`) with the
  person's consent, which names the site. The WhatsApp template tells agents never to read or send chats.

**Limits**
- Desktop app only — sites refuse to be shown inside another page. In Cowork in Chrome, its installed web
  app or on a phone, opening a site app shows "\<title> is part of the desktop app" (Get the Desktop App /
  Not Now) and the tab closes; a site app in a side bar says so in its pane.
- In the right side bar the app's tab shows its title, not its icon or color. Dragging the tab to another
  side bar is remembered for you and wins over `display`.
- A site notifies only while its tab is open: closing the tab stops it (a window hidden to the tray
  keeps it running), and there is no Web Push. Only the active workspace's site apps run; switching
  workspaces closes the others. At most 6 banners per app per minute.
- A notification a site's own background worker raises is shown by the system without the app's name or
  mute. The page's own notifications are the ones Cowork attributes.
- The unread count is on the dock or taskbar, not on the app's rail icon, and a site that does not set a
  badge gets the `(N)` from its page title. The rail icon itself arrives with the next engine release;
  until then open a site app right after installing it, from Manage Workspace, with `cowork app open`
  or from its notifications.
- No address bar on a page outside `scope`. Switching accounts or a crash reload loses the page's state.
- Ignored from the site's manifest: shortcuts, file and protocol handlers, share target, window-controls
  overlay. A site that checks the browser's user agent may need a fix from Cowork's side.
- Install refuses `localhost` and private-network addresses when the start page is public.

## Examples

A mail client — sidebar list + a routine-materialized editor view + a routed file type:

```json
{
  "name": "gmail",
  "version": "1.0.0",
  "title": "Gmail",
  "description": "Read and triage your inbox without leaving Cowork.",
  "icon": "mail",
  "color": "#EA4335",
  "entry": "gmail-sidemenu.jsx",
  "display": "sidebar",
  "open": { "routine": "gmail-open-inbox", "args": { "folder": "INBOX" } },
  "routines": {
    "gmail-open-inbox": "open-inbox.ts",
    "gmail-fetch": "fetch.ts",
    "gmail-send": "send.ts"
  },
  "fileRouting": { "**/*.maillist.json": "mail-list.jsx" },
  "tags": ["email", "google"]
}
```

A dashboard in a tab:

```json
{ "name": "sales-dashboard", "version": "0.1.0", "title": "Sales", "icon": "graph", "color": "#0F9D58",
  "entry": "dashboard.jsx", "display": "tab", "routines": { "sales-load": "load.ts" } }
```

A quick-capture dialog:

```json
{ "name": "quick-note", "version": "0.1.0", "icon": "note", "entry": "quick-note-modal.jsx", "display": "modal" }
```

A capability — a key to connect, and routines marked as tools for agents:

```json
{ "name": "invoices", "version": "1.0.0", "title": "Invoices", "kind": "capability",
  "credential": { "key": { "fields": ["token"], "url": "https://billing.example.com/settings/api", "hint": "Settings › API › New token" } },
  "routines": {
    "invoices-list": { "file": "list.ts", "readOnly": true, "description": "Lists open invoices" },
    "invoices-approve": { "file": "approve.ts", "reachesOthers": true, "description": "Approves an invoice and emails the supplier",
                          "input": { "type": "object", "properties": { "id": { "type": "string", "description": "The invoice id" } }, "required": ["id"] } }
  } }
```

A headless app (routines + a viewer for a file type, no icon):

```json
{ "name": "csv-tools", "version": "1.2.0", "routines": { "csv-summarize": "summarize.ts" },
  "fileRouting": { "**/*.csv": "csv-viewer.jsx" } }
```

## Footer item

`statusBar` puts one item in the footer, left, after `Workspace: <name>`. Its routine returns
what to show — or `null` to hide it:

```ts
export default async (args, ctx) => args.action === "disconnect"
  ? (await disconnect(), null)                      // a click's work; the item refreshes after
  : (await isConnected())
    ? { text: "jotas@34.135.78.28", icon: "remote", tooltip: "Connected over SSH",
        click: { args: { action: "disconnect" }, confirm: "Disconnect from the VM?" },
        data: { connected: true } }
    : { text: "Connect to VM", icon: "plug", click: { args: { action: "connect" } },
        data: { connected: false } };
```

- `text` (required), `icon` (a codicon; `sync~spin` spins), `tooltip`, `tone`: `"warning"` |
  `"error"` (colors the item).
- `click: { args, confirm? }` — clicking runs the SAME routine with `args` (after a modal when
  `confirm` is set) while the item spins; a thrown error shows as a notification. `menu: [{ label,
  args, confirm? }]` instead lets the user pick one action. Either way the item refreshes after.
- `data` — free-form (≤ 16 KB) for the app's widgets: `cowork.statusBar.get("<name>").data`.
- It refreshes when it appears, every `interval` seconds while the window has focus, when focus
  returns, after a click, and on `cowork.statusBar.refresh(name)`. A failing refresh shows the
  app's title as a warning (the error in its tooltip; a click retries).
- Each refresh is a routine run (audited, tagged `status-bar`): keep the routine cheap — a local
  check, not a network round trip — and the interval as long as you can live with.

## Folder layout

```
.cowork/apps/gmail/
  cowork.json
  gmail-sidemenu.jsx      ← entry (naming: *-sidemenu.jsx / *-modal.jsx is convention only)
  mail-list.jsx           ← fileRouting target
  open-inbox.ts  fetch.ts  send.ts   ← routines
  README.md               ← optional; travels with the app
  SKILL.md                ← optional; how an agent uses the app (linked as .claude/skills/<name>)
```

`SKILL.md` tells an agent how to USE the app:
- its files and data;
- setup and environment (test vs production);
- the routines to run;
- the rules that bite.

Its front matter is `name` (the app's `name`) and a one-line `description` ending in "Use when …".
Cowork links every enabled app that has one as the project skill `.claude/skills/<name>`, pointing at
the app folder, so Claude Code knows the app from the start of a session. Keep it short: the README
stays the full reference.

Everything in the folder travels when the app is pushed/installed. Keep generated data (caches,
exports, `.db` files that hold user data) OUT of the app folder — write them elsewhere in the
workspace (`ctx.data` writes to `.cowork/data/<app>/`; `cowork table create <name>` makes
`.cowork/data/<name>.db`), so an update never collides with user data and an edited app stays "clean".

## Collisions

- Routine ids are ONE namespace per workspace: first app folder wins. A fileRouting glob two apps claim
  (Docs and WordCraft both `**/*.docx`) asks the user once which app opens it and remembers the answer in
  `.cowork/workspace.json` `openWith`; until then the first switched-on app is the route.
  **Prefix routine ids with the app name** (`gmail-fetch`, not `fetch`).

## `.cowork/workspace.json` — workspace STATE, not registrations

The file holds state only (it never travels with an app or a published workspace); the keys:

```json
{
  "publishTo": "<team-server slug>",
  "startup": { "home": "gmail" },
  "disabled": { "apps": [".cowork/apps/gmail-v1-2-3"], "routines": ["gmail-send"] },
  "rail": { "folders": [{ "id": "google", "title": "Google", "color": "#0090ff",
    "members": ["workbench.view.cowork.app.gmail", "workbench.view.cowork.app.calendar"] }] },
  "deployments": { "gmail-sync": { "target": "cloudflare-worker", "mode": "local", "schedule": "0 * * * *" } }
}
```

- `disabled.apps` takes an app FOLDER (switches off its icon, routes and routines — including
  files a later update adds) or a single file; `disabled.routines` takes routine ids (listed but
  refuse to run). Off-only switches; safe to commit.
- `rail.folders` groups activity-bar icons into folders (members are view-container ids; an
  app's is `workbench.view.cowork.app.<name>`). Users make them by dragging one icon onto
  another. A folder holds icons of one rail group — app icons and
  the system icons never share one.
- `publishTo` is the default team server for `cowork push` — set it with `cowork push --to <ts> --remember`
  (or "Use as this workspace's default" when publishing from Manage Workspace).
- `startup.home` names the app the window opens on (default: the app named `home`); `startup.left` /
  `right` (booleans) open or close the side bars at start.
- Nothing else registers anything: containers, file routes, routines and open modes come from the app's
  `cowork.json`, so the app is self-contained and updates re-register it automatically.
