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).
| 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): 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
Section titled “Display modes”sidebar—entryrenders in a side panel under the app's icon. Optionalopenputs something in the editor area too (once per session, so the user isn't yanked around).tab— clicking the icon opensentryin an editor tab (no side panel), every click.modal— clicking opensentryin a resizable in-workbench dialog.
Web apps
Section titled “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):
{ "name": "wordcraft", "version": "0.10.0", "title": "WordCraft", "icon": "icon.png", "web": { "index": "web/index.html" }, "fileRouting": { "**/*.docx": "web/index.html" } }web.indexis 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 intoweb/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).indexis 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 asweb/<index>still wins. displayis alwaystab; there is noentry, no widget and nowindow.coworkSDK — the page is the app. Routines andSKILL.mdwork as in any app.- A routed file (
**/*.docxabove) opens in the web app's tab. Open the app directly withcowork 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
Section titled “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:
{ "name": "whatsapp", "version": "1.0.0", "title": "WhatsApp", "icon": "icon.png", "web": { "url": "https://web.whatsapp.com/", "scope": "https://web.whatsapp.com/" } }web.urlis an https page (http only on localhost) with no credentials in it.scopeis an https prefix on the same origin; it defaults tourlup to its last/. A manifest hasweb.index(a web app) orweb.url(a site app), not both, andfileRoutingcannot 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 withcowork 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 rightinstalls it there (chatgpt.com and claude.ai turn scripts away, so the CLI installs them from the address with no icon —--titlenames 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 (openis a sidebar app's only — it runs from a rail click).
{ "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>, andcowork.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, withcowork app openor 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
localhostand private-network addresses when the start page is public.
Examples
Section titled “Examples”A mail client — sidebar list + a routine-materialized editor view + a routed file type:
{ "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:
{ "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:
{ "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:
{ "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):
{ "name": "csv-tools", "version": "1.2.0", "routines": { "csv-summarize": "summarize.ts" }, "fileRouting": { "**/*.csv": "csv-viewer.jsx" } }Footer item
Section titled “Footer item”statusBar puts one item in the footer, left, after Workspace: <name>. Its routine returns
what to show — or null to hide it:
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~spinspins),tooltip,tone:"warning"|"error"(colors the item).click: { args, confirm? }— clicking runs the SAME routine withargs(after a modal whenconfirmis 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
intervalseconds while the window has focus, when focus returns, after a click, and oncowork.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
Section titled “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
Section titled “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.jsonopenWith; until then the first switched-on app is the route. Prefix routine ids with the app name (gmail-fetch, notfetch).
.cowork/workspace.json — workspace STATE, not registrations
Section titled “.cowork/workspace.json — workspace STATE, not registrations”The file holds state only (it never travels with an app or a published workspace); the keys:
{ "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.appstakes an app FOLDER (switches off its icon, routes and routines — including files a later update adds) or a single file;disabled.routinestakes routine ids (listed but refuse to run). Off-only switches; safe to commit.rail.foldersgroups activity-bar icons into folders (members are view-container ids; an app's isworkbench.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.publishTois the default team server forcowork push— set it withcowork push --to <ts> --remember(or "Use as this workspace's default" when publishing from Manage Workspace).startup.homenames the app the window opens on (default: the app namedhome);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.