Will my plugin's hooks and scripts run in Cowork?
Yes — plugin hooks do fire in Cowork. The determinant is where the plugin was installed from: a plugin installed through the Cowork application is visible to Cowork tasks, and one installed through the CLI's own path is not, because they occupy different namespaces. The plugin-root token resolves differently depending on which consumer expands it, so use it for file-tool reads and discover the mounted path at runtime for anything you shell out to.
Do not use a hook to export environment variables for your skill's shell commands.
Hooks run, but the environment they modify is not the environment those commands see. This is a common way a plugin appears broken in Cowork while working in the CLI: the hook fires, the export succeeds, and the value is simply absent downstream.
As of the 2026-08-05 capture · Desktop 1.25927.0 · not individually re-checked since this rule last changed
Expect plugin hooks to fire in Cowork.
Hooks shipped by a plugin do run in Cowork, in both the local and the cloud lane: in live checks, prompt, before-tool, after-tool and stop hooks fired on both lanes, and the session-start hook on the local lane. Two things make it look otherwise. A plugin installed where Cowork tasks do not look appears to have no hooks at all. And a hook that succeeds without printing anything leaves no trace in the session's record, so check a side effect it produces rather than the record.
Checked live 2026-09-28 · Desktop 2.9939.2 · agent 2.1.281
Match the shell under both of its names in a before-tool hook.
The shell tool is called one thing in the command-line tool and in a cloud session, and another in a local session. The agent translates between the two when matching hooks only if the session carries a mapping between the names, which current desktop builds pass but older ones and other launchers may not. Listing both names in the matcher works either way, on both lanes.
Checked live 2026-09-25 · Desktop 2.9939.2 · agent 2.1.281
Ship every script your hook commands name inside the plugin: on Linux a missing script can block tool calls.
When a hook runs a script that is not there, the exit code depends on the shell. The usual shell on Debian and Ubuntu exits with 2, the code a hook uses to block; the macOS shell exits with a different code that does not block. Claude Code recognises the missing-script case only for stop-type hooks and a plugin's prompt hook, and shows a warning with a hint to reinstall the plugin instead of blocking. For every other event, a before-tool hook included, the 2 is taken as a real block, so every tool call the hook matches is refused. A hook that only warns when tried on a Mac can block on Linux.
Re-read from the shipped code 2026-09-27 · Desktop 2.9939.2 · agent 2.1.281
To get a personal skill into Cowork, upload it as a skill rather than packaging it as a plugin.
A skill uploaded from the Skills section of the desktop application's Customize screen becomes part of the user's account. Within about a minute it reached the command-line tool, a cloud session and a local one, each time with its full instructions, its scripts and its reference files, and the scripts ran. Zip the skill with its folder at the top level; the uploader refuses a zip that also contains a plugin manifest.
Checked live 2026-09-27 · Desktop 2.9939.2 · agent 2.1.281
A user can create and change their own skills by asking Claude in Cowork; point them there before suggesting a re-upload.
In a Cowork task that is allowed to save skills, asking Claude to create a skill, or to change one the user already has, produces a card the user confirms; the result is saved to their account and reaches their other sessions and the command-line tool within about a minute. A change replaces only the skill's instructions and keeps its other files, so an uploaded skill with scripts can be edited this way. A new skill created in chat has instructions only. One that needs scripts or reference files can be uploaded, or, in a cloud session, packaged as a .skill file and sent to the user: the Save skill button on that file installs every file.
Checked live 2026-09-29 · Desktop 2.9939.4 · agent 2.1.284
Do not rely on a prompt-blocking hook to tell the user why their message was stopped.
A plugin hook that blocks a prompt works in Cowork: the turn stops and the model never sees the message. On the user's own machine the app shows the hook's reason, though it presents it as an organization policy. In the remote sandbox the user sees nothing at all: their message stays, with no reply and no notice. If the user needs to know, block at the tool level instead, where the model sees the denial and can explain it.
Checked live 2026-09-28 · Desktop 2.9939.2 · agent 2.1.281
Do not pass a credential to a plugin's server through an environment placeholder.
In a plugin's server configuration, only a few standard variables are filled in: the home folder, the search path, the shell, the user and the terminal, plus the plugin's own root. Any other placeholder reaches the server as its literal text in both Cowork sandboxes, even when the variable is set on the user's machine, and a placeholder with a default gets the default instead. The server still starts, so the failure shows up later as rejected requests. Have the server read its credential itself, for example from a file in the user's home folder or from the system keychain, and treat a value that still looks like a placeholder as missing. A server the user adds to the desktop application's own MCP configuration, with the key in that server's environment block, does receive it.
Checked live 2026-09-28 · Desktop 2.9939.2 · agent 2.1.281
Install a plugin through the Cowork application if you want Cowork tasks to see it.
Plugins installed through the CLI's own installation path land in a namespace that Cowork tasks never read, so the plugin appears installed and is simply invisible. This is the usual explanation for 'my plugin works in the CLI but not in Cowork'.
As of the 2026-08-05 capture · Desktop 1.25927.0 · not individually re-checked since this rule last changed
Do not write into your plugin's own directory at runtime.
Plugin content is mounted for reading. Treat everything shipped with the plugin as immutable during a session and put generated state in the outputs location.
As of the 2026-08-05 capture · Desktop 1.25927.0 · not individually re-checked since this rule last changed
Expect plugin files to be present as a mounted directory, and find it rather than hardcoding it.
Plugin content is made available to the session as its own mount. The exact location embeds identifiers that vary per plugin and per install source, and the shape has changed across releases, so discovery at runtime is the only stable approach.
As of the 2026-08-05 capture · Desktop 1.25927.0 · not individually re-checked since this rule last changed
Use ${CLAUDE_PLUGIN_ROOT} for file-tool reads, and discover the mounted path at runtime for anything you execute.
${CLAUDE_PLUGIN_ROOT} is a token that expands to your plugin's directory — but it expands differently depending on what expands it and which sandbox the session is in, and one expansion names a location the other cannot reach. Reading your plugin's files through the file tools with the token is reliable only where the runtime replaces it for you, which it does in the files it loads as definitions — your skill's own body, a command, a hook. There the token is swapped for a real path before the model ever sees it. It is not replaced in a file your skill merely reads later while it runs: a reference file that instructs the model to use the token hands over the token itself, which names nothing. This is the difference between text the runtime loads and text your skill reads, and it has cost a real skill a silent fallback to a degraded path. Executing a script by that path is not: in the local sandbox the substituted value names a host staging directory the shell cannot see. And do not test for the obvious failure — it is not the one you get. The token is often *present* in a shell, holding a different plugin's directory: hooks can write environment for later shell commands, each hook is given its own plugin's location, and the last one to write wins. So a shell reading it typically fails as a plausible wrong path, not as a blank — checking whether it is empty comes back clean and sends you looking in the wrong place. For anything you execute, do not read the token at all: run the script by the absolute path your skill body was given, and where the shell cannot open that path, locate the plugin's directory from the shell's side at run time.
As of the 2026-08-14 capture · Desktop 1.30096.1 · not individually re-checked since this rule last changed
Do not ship a launcher in your plugin's bin/ if the plugin may go through an organization; run your scripts by path.
Claude Code does put every enabled plugin's bin/ directory on the shell's command path, in the shell's own terms, and on a GitHub or local command-line install a launcher you commit there works as a bare command. It is still not a channel to build on. Organization distribution on claude.ai rejects any plugin that has a top-level bin/ — through marketplace sync and direct upload alike — because those entries reach the command path without appearing on the administrator's approval screen. The plugin validator does not warn about it, even in strict mode, and the administrator sees only a generic "Marketplace sync failed" message; the real reason is in the desktop app's log. And in Claude Cowork on the desktop, no launcher an author committed has been seen on the shell's command path: the only populated directory there was one the runtime wrote itself. So run bundled scripts by path: use the absolute path your SKILL.md body is given, and where the shell is a different mount of the same files (Cowork on the desktop), locate the script from the shell's side with a search.
bin/ inside a skill directory, or at the outer root of a marketplace repository, is not what it checks, and an ordinary project CLI elsewhere in a repository is fine where it is.Checked live 2026-09-01 · Desktop 1.40609.1
${CLAUDE_SKILL_DIR} works in your SKILL.md body and nowhere else.
It is the variable most skill authors reach for to point at their own bundled files, and in the body of SKILL.md it does exactly what you want: it is replaced with a real absolute path before the model ever reads the text, so a file tool opens the right file. There are four places it will quietly do nothing instead. In a shell command it is not set at all, so it becomes empty and the command addresses a path at the root of the filesystem. Written without braces it is not recognised and survives into the loaded text as the literal characters you typed. And in a reference file your skill opens while it is running — as opposed to the definition the runtime loads at startup — it is never substituted, so it arrives as the characters you typed. And in a command file — which is definition text the runtime loads, and where the other tokens are replaced normally — this one alone is skipped. One rule covers all four: it belongs to the body of a skill specifically, not to definition text in general, and not to anything that can be looked up later. Once a path has to reach a shell, the model passes an absolute path it has already resolved — and where the shell is a different mount of the same files, a path it located from the shell's side.
${CLAUDE_SKILL_DIR}, ${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA} and $ARGUMENTS left exactly as written, together with a 'Base directory' line naming a folder that does not exist (measured on 2026-10-02 on the web and the desktop app). Only the skill's own text is affected: the plugin's agents, hooks and files work as usual in that same turn. If your skill runs scripts in the cloud, give the script path a fallback that finds the skill's folder when the token comes through unreplaced.As of the 2026-08-14 capture · Desktop 1.30096.1 · not individually re-checked since this rule last changed
Your skill's description is the only thing that decides whether it gets used.
Everything else you write is read after that decision, if it is read at all. Two frontmatter settings can stop a skill being reachable before any of it is considered: one restricts it to loading only when the model touches files matching a pattern, so a question that touches no files quietly gets nothing; another tells the model to ask you to run the skill rather than using it. There is also a second field that looks made for describing when to reach for a skill, and it does feed the decision — but only in the terminal. The desktop listing has no handling for it at all, and other tools that read skills ignore it. So put what makes your skill findable in the description, keep it self-sufficient, and treat the other field as reinforcement you could delete without loss.
As of the 2026-08-14 capture · Desktop 1.30096.1 · not individually re-checked since this rule last changed
If your plugin ships an MCP server, make it a stdio command with no user-configuration placeholders, and expect it to run on the user's machine rather than in the sandbox.
In the remote sandbox the session itself does not start plugin MCP servers. The Claude Desktop app on the user's machine starts them — the same bridge it uses for servers in its own MCP configuration — and announces their tools into the session, so every call travels back to the user's machine and is gated by the Desktop app's own tool-approval prompt, not by anything in your skill or your plugin's permission rules. Only a stdio command qualifies: a server declared by URL is skipped, so an HTTP-only endpoint needs a stdio-to-HTTP shim (the mcp-remote package is the one observed working); a server whose configuration references plugin user-configuration placeholders is skipped; and one bundled Anthropic plugin is excluded from the bridge and served from inside the sandbox instead. The Desktop app must stay open for the whole session — close it and the tools vanish mid-conversation — and after a server outage the bridge's own reconnect decides when they come back, measured once at seventeen minutes.
As of the 2026-09-05 capture · Desktop 1.46388.4 · not individually re-checked since this rule last changed
Do not name a server memory in the desktop application's MCP configuration; a plugin's own memory server is safe.
Current Cowork desktop builds install their own server under the name memory, backed by the user's cloud memory. A server configured under that same name in the desktop application's MCP configuration — a common name for the reference memory server — is replaced by it, and never starts. The only trace is a line in the desktop application's log. A server that ships inside a plugin is not affected, because plugin servers are loaded under a name that includes the plugin's, so it runs next to the cloud one. Its tools are distinct from the built-in memory tools, but the model now sees two sets of memory tools; a distinctive name avoids that confusion.
Re-read from the shipped code 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
Do not build an Artifact that calls the user's local MCP servers. It cannot today, even inside the desktop application.
The desktop application contains a channel that lets a rendered Artifact call the servers in its MCP configuration, and its switch is on. But publishing refuses an Artifact that declares a local server, and the Artifact viewer refuses any call to a local server the Artifact did not declare, before the desktop application is involved. To reach the user's machine, use a skill or a plugin's own server. Collect a secret through the server's own prompt to the user (elicitation), never through a form in an Artifact.
Checked live 2026-09-27 · Desktop 2.9939.2
Expect a server in the desktop application's MCP configuration to run as two separate processes, and keep no state only in memory.
The desktop application starts a server listed in its MCP configuration once for each of its clients: one copy for chat and one for Cowork, each with its own connection and its own memory. A value one copy holds in memory is invisible to the other, and which copy answers depends on where the call came from. Keep shared state on disk or in a service, and make startup safe to run twice at the same time.
Checked live 2026-09-27 · Desktop 2.9939.2
Ship Cowork skills in a plugin, not in a project's .claude/skills folder.
A skill left in a folder's .claude/skills does not load in a local session, as far as the shipped code shows: the desktop starts the agent with user settings only, and project skills load from project settings. In a cloud session that the user grants the folder to, the desktop does upload those skills, but by default only as stubs: the model sees your frontmatter, the body is replaced by a note to run the skill on the user's device, and your scripts never run in the cloud. Past 100 skills in one folder, or 32 MiB in total, skills are dropped with little or no notice. A plugin is the only route that delivers the whole skill on both lanes.
Re-read from the shipped code 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
Expect your plugin's local MCP server to work in Cowork, and a remote one to be replaced when the user already has that service connected.
A plugin's MCP server that runs locally over stdio gets its real tools in a local session: the desktop application starts it on the user's machine and hands its tools to the session. Two things replace a server with an empty stand-in that lists no tools. An organisation's MCP policy can do it to local servers and packaged extensions. And a remote server, one reached over HTTP, is replaced when the user already has the same service connected through a claude.ai connector, because the connector then serves it. Earlier desktop releases replaced every remote plugin server while that behaviour was switched on. The desktop application's log records each replacement by name, which is the quickest way to tell which case you are in.
Checked live 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
Do not write exact MCP tool names for connectors or a plugin's remote servers into your skill.
In Cowork, the tools of a claude.ai connector are named after the organisation's own record for that connector, an identifier that differed between the two organisations observed, not after the service. A plugin's own server has names built from the plugin's name, but when the organisation also has the same service connected, the plugin's copy is replaced and those names disappear. A skill that spells out either form is tied to one organisation's setup. Describe the tool by what it does and let the model pick the matching tool.
Checked live 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
When testing a plugin you also have installed, check which copy actually loaded.
A copy passed on the command line with --plugin-dir silently replaces the installed plugin of the same name — the whole plugin, skills, hooks and servers alike — and nothing tells you. The installed copy wins instead if the command-line copy is disabled, if an administrator has locked that plugin, if side-loading is turned off, or if the names differ, even only in letter case, in which case both load. The session's list of loaded plugins shows each copy's path; that path, not the plugin's source label, tells you which one you are testing.
Re-read from the shipped code 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
Put your skill's trigger words in its name, and keep its description short.
The list of skills the model sees has a size budget: about 30,000 characters on current models with their standard 1M context, but only 6,000 to 8,000 when the model runs a 200k context. When a user has more skills than fit, bundled skills keep their descriptions and the rest are ranked by how recently and often they were used; those that do not fit are listed by name alone. A new or rarely used skill is the first to lose its description, and a long description can lose to a shorter one even when it ranks higher. A skill whose name says what it does still gets picked.
Re-read from the shipped code 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
Do not mark a hook to run for cloud sessions unless you accept that the session may have written what it runs.
When a user drives a cloud session from their own machine, the command-line tool can offer that session some of the user's hooks, run on the user's machine. By default only a hook whose script can be fingerprinted and lies outside anything the cloud session can write is offered. A hook marked to run on the device anyway can end up running a script, or a helper it loads, that the cloud session itself wrote. The user still has to consent on that machine, and a fingerprinted script that changes afterwards is refused.
Re-read from the shipped code 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
Do not conclude your hook did not fire because a local session's records do not show it.
The agent reports hook activity in its output stream only for session-start hooks unless it is started with an option the desktop application never passes. So a local session's records show session-start hooks and nothing else, even when your other hooks ran. Check your hook's own side effects instead, or reproduce outside Cowork with hook events switched on. Cloud sessions report every hook.
Re-read from the shipped code 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
Do not rely on a forked skill's exact output reaching the user.
A skill set to run as a separate sub-task hands back only its final message, and the main conversation then writes its own answer from it — shorter, reworded, and in measured runs usually without the skill's links and caveats. Asking the main model, from inside the skill, to pass something through helps only partly and can make it warn the user about a possible prompt injection. For output whose exact wording matters, run the skill inline, which has no relay step, or write the essential content to a file the skill produces.
Checked live 2026-09-23 · Desktop 2.7032.0 · agent 2.1.280
What is not established
- Plugin-root resolution differs between the two sandboxes. The plugin-root rule is written to be correct in both, at the cost of being less direct than a sandbox-specific answer would be.