[BUG] Cowork sessions ignore user hooks and managed settings — sandbox platform mismatch breaks all settings resolution
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
Cowork (local-agent-mode) sessions silently ignore all three settings sources: user settings (~/.claude/settings.json), managed/MDM settings (/Library/Application Support/ClaudeCode/managed-settings.json), and environment variable overrides. This means user-defined hooks never fire, enterprise managed policies are not enforced, and env-based configuration (e.g. CLAUDE_CODE_EFFORT_LEVEL) has no effect.
This is distinct from #27398 (plugin hooks via --setting-sources user). That issue covered plugin-scoped hook discovery being excluded by the --setting-sources flag. This issue covers three additional, independent root causes that affect all settings — not just plugin hooks — and persist even if --setting-sources were fixed.
Root Cause 1: User settings file doesn't exist in the sandbox
The sandbox sets CLAUDE_CONFIG_DIR=/sessions/<name>/mnt/.claude. In cowork mode, the binary looks for cowork_settings.json (via the WK5() / getSettingsFilename() function that checks isCowork()). This file does not exist in the sandbox — only .claude.json is present.
The user's actual ~/.claude/settings.json on the macOS host is never mounted into the VM.
Verified from inside the sandbox:
$ echo $CLAUDE_CONFIG_DIR
/sessions/eloquent-upbeat-albattani/mnt/.claude
$ ls $CLAUDE_CONFIG_DIR/settings.json
No such file or directory
$ ls $CLAUDE_CONFIG_DIR/cowork_settings.json
No such file or directory
Root Cause 2: Managed settings path resolves to wrong location (platform mismatch)
The managed settings directory is resolved by process.platform:
// Reconstructed from compiled binary
function getManagedSettingsDir() {
switch (getPlatform()) {
case "macos": return "/Library/Application Support/ClaudeCode";
case "windows": return "C:\\Program Files\\ClaudeCode";
default: return "/etc/claude-code"; // Linux falls here
}
}
The cowork sandbox is a Linux VM (Ubuntu 22.04 aarch64), so process.platform is linux, and the binary looks for /etc/claude-code/managed-settings.json. This path does not exist in the sandbox. The macOS host path (/Library/Application Support/ClaudeCode/) is never consulted.
Note: CLAUDE_CODE_HOST_PLATFORM=darwin is set as an env var, but the binary uses process.platform (which is linux), not this env var, for path resolution.
Verified from inside the sandbox:
$ uname -s
Linux
$ ls /etc/claude-code/
No such file or directory
$ ls /Library/
No such file or directory
$ echo $CLAUDE_CODE_HOST_PLATFORM
darwin
Root Cause 3: User environment variables are not forwarded into the sandbox
The sandbox receives a curated set of env vars (CLAUDE_CODE_IS_COWORK, CLAUDE_CONFIG_DIR, OAuth tokens, proxy ports, etc.), but user-set environment variables like CLAUDE_CODE_EFFORT_LEVEL from the macOS host are not forwarded.
$ echo $CLAUDE_CODE_EFFORT_LEVEL
(empty)
This means the documented workaround from #34428 (CLAUDE_CODE_EFFORT_LEVEL=max in settings.json env block) doesn't work for cowork — settings.json isn't loaded (Root Cause 1), and even if set in the host shell, the env var isn't forwarded (Root Cause 3).
Impact
| Settings Source | Interactive CLI | Headless (-p) | Cowork | Root Cause |
|---|---|---|---|---|
| ~/.claude/settings.json hooks | Fire | Fire | Silent no-op | Config dir has no settings file |
| Managed settings (MDM) | Loaded | Loaded | Silent no-op | Platform mismatch → wrong path |
| CLAUDE_CODE_EFFORT_LEVEL env | Works | Works | Ignored | Env not forwarded to sandbox |
| .claude.json (OAuth/flags) | Loaded | Loaded | Loaded | ✅ Mounted via bindfs |
Enterprise concern: Managed settings are designed for policy enforcement via MDM profiles. Having them silently ignored in cowork means enterprise admins cannot enforce hooks, permission rules, or other policies in cowork sessions — a compliance gap.
What Should Happen?
- User hooks from
~/.claude/settings.jsonshould be mounted/copied into the sandbox ascowork_settings.json(or the binary should read from a host-provided path) - Managed settings from the macOS host should be bind-mounted into the sandbox at
/etc/claude-code/managed-settings.json(the Linux-expected path), since the binary already knows how to load from there - User env vars relevant to Claude Code (at minimum
CLAUDE_CODE_EFFORT_LEVEL) should be forwarded into the sandbox
Error Messages/Logs
No errors — all three failures are silent. Settings resolution finds no file and returns empty/default. This is part of the problem: there's zero indication that settings are being ignored.
Steps to Reproduce
- Add hooks to
~/.claude/settings.jsonon the macOS host (e.g., aPostToolUsehook that writes to a log file) - Confirm the hooks fire in an interactive
claudeCLI session - Start a Cowork session in Claude Desktop
- Trigger the same hook event (e.g., use a tool)
- Observe that hooks never fire — no log file is written, no hook output appears
- Optionally verify from inside the sandbox (if you can get shell access) that
$CLAUDE_CONFIG_DIR/cowork_settings.jsonand/etc/claude-code/managed-settings.jsondon't exist
Claude Model
claude-opus-4-6
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
Cowork VM binary at /usr/local/bin/claude (version embedded in Bun-compiled binary, matches latest Claude Desktop release)
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Other
Additional Information
Related issues:
- #27398 — Cowork plugin hooks not firing due to
--setting-sources user(closed as duplicate, different root cause from this issue) - #32364 — OTel config not available in sandboxed environments (same underlying problem: settings.json inaccessible in sandbox)
- #34428 — Effort level not persisted (workaround via settings.json
envblock doesn't work in cowork)
Key environment variables in the cowork sandbox:
CLAUDECODE=1
CLAUDE_CODE_IS_COWORK=1
CLAUDE_CODE_ENTRYPOINT=local-agent
CLAUDE_CONFIG_DIR=/sessions/<name>/mnt/.claude
HOME=/sessions/<name>
CLAUDE_CODE_HOST_PLATFORM=darwin
SANDBOX_RUNTIME=1
Sandbox architecture: Linux VM (Ubuntu 22.04 aarch64) using bubblewrap (bwrap) for process isolation. Host directories are selectively bind-mounted via bindfs FUSE mounts with controlled permissions. The .claude/ directory is mounted read-write but only contains .claude.json — no settings.json or cowork_settings.json.
21 Comments
Found 3 possible duplicate issues:
This issue will be automatically closed as a duplicate in 3 days.
🤖 Generated with Claude Code
This is not a duplicate of any of the three flagged issues. Here's why:
#39889 — Covers Dispatch (mobile-spawned) sessions ignoring model/permission settings. Different feature (Dispatch, not Cowork), different root cause (Electron env stripping vs. VM sandbox platform mismatch). That issue doesn't mention hooks, managed settings, or the Linux VM platform detection problem.
#39125 — Covers
user_configenv vars not being injected into MCP server processes in cowork. While it shares the sandbox isolation theme, it's specifically about MCP credential template resolution (${user_config.ct_username}), not about user hooks, managed/MDM policy settings, or theprocess.platformmismatch that causes the binary to look at/etc/claude-code/instead of/Library/Application Support/ClaudeCode/.#33613 — Covers
managed-settings.jsonbeing bypassed in VSCode + remote-SSH. Completely different execution context (VSCode extension deploying a binary to a remote host) with a different root cause (remote host doesn't have the managed-settings file). Our issue is about the Cowork VM sandbox where the file exists on the macOS host but the binary inside the Linux VM resolves to the wrong platform-specific path.What this issue uniquely documents:
process.platformmismatch: the Cowork Linux VM causes managed settings to resolve to/etc/claude-code/(doesn't exist) instead of using the macOS host's/Library/Application Support/ClaudeCode/(exists and works for CLI sessions)cowork_settings.jsonfile that the binary explicitly looks for in cowork mode (via theisCowork()check) but that is never created by the sandbox launcherThese are related issues in the broader "sandbox isolation breaks settings" category, but they have distinct root causes and require distinct fixes.
Production impact: enforcement engine blocked by this bug
Platforms affected: Windows (host), Linux (Cowork sandbox), macOS (same sandbox architecture)
I'm running an autonomous platform orchestration layer on Cowork — Windows 11 host, Linux sandbox (where Cowork actually executes), PostgreSQL backend on Hetzner VPS, 9 agents via OpenFang, deployment pipelines, scheduled tasks. On the Claude Code side (Windows), I have a 55-rule enforcement engine (
enforcement-engine.ps1, 2,500 lines) with hash-chained tamper-proof logging that runs viaPreToolUsehooks on every tool call. Exit code 2 = hard block. The model physically cannot bypass it.In Cowork, none of this works — on any host OS. The sandbox is a Linux VM regardless of whether the host is Windows, macOS, or Linux. Hooks silently don't fire. The enforcement engine can't run. Every constraint becomes advisory text loaded at session start that the model follows voluntarily.
What actually happened
During a Cowork session today, I had the model implement six features (tiered memory loading, lease-based task queues, cost estimation, provider abstraction, tool taxonomy, verbatim context storage). The model wrote the code, ran e2e tests locally, verified syntax — then stopped. It said "the file watcher should auto-deploy on next git push" without actually committing or pushing anything. The code sat on disk, unreachable by the production server.
The constraint banning that exact behavior (
constraint:no_guessing_deploy_state) was filed to PostgreSQL by the model itself, 10 minutes earlier, in the same session. Knowledge wasn't the problem. There was no enforcement mechanism to execute it.On Claude Code, this can't happen — R-049 in the enforcement engine pattern-matches deploy-related tool calls and blocks anything that doesn't go through the deploy gate. In Cowork, that rule is just text.
What I built as a workaround
I exhausted every alternative before arriving here:
implementation-gate.pythat outputs binary PASS/FAIL for syntax/commit/push/deploy/PG-status checks. ~80% effective. Not mechanical.The gate script works:
But the model can still skip running it. Nothing forces the call. That's the gap only a
PreToolUsehook can close.Why fixing this bug matters beyond my use case
This isn't just about my platform. The fundamental issue is: Cowork is positioned as an autonomous agent, but autonomy without enforcement is a liability. The model-behavioral safety layer ("trained to follow rules") is necessary but not sufficient for workflows where mistakes have real consequences — deploying broken code, skipping security checks, sending messages to real clients (see also #41184 where MSApps hits the same wall running 200+ Cowork sessions with 54 scheduled tasks in production).
The hook infrastructure already exists in Claude Code. The three root causes you identified (settings file not mounted, platform mismatch on managed settings path, env vars not forwarded) are all sandbox configuration issues, not missing architecture. Fixing them would immediately unlock mechanical enforcement for every Cowork user running production workloads.
What I'd build the day this ships
The enforcement engine is already written. The rule patterns are in PostgreSQL. The moment
PreToolUsefires in Cowork, I wire it to the same gate:55 rules, dynamic pattern loading from PG, hash-chained audit log — all functional on day one. The code exists. The sandbox configuration is the only blocker.
+1 on this. Adding a concrete use case from the feature request side:
I have a SessionStart hook in ~/.claude/settings.json that mounts Google Drive and runs a session-autoload skill to restore persistent memory from an Obsidian vault. Works perfectly in CLI. In Cowork, it silently does nothing.
The only workaround is putting the same instructions in CLAUDE.md as a "BLOCKING" directive, but Claude routinely ignores it in favor of answering the user's first message. So the real workaround is training myself to type "boot up" as a throwaway first message every session — which defeats the purpose of having a hooks system.
This isn't just a convenience issue. Without SessionStart hooks, there's no reliable way to initialize session context in Cowork. CLAUDE.md is advisory, scheduled tasks don't have a "session start" trigger, and skills only fire when explicitly invoked. Hooks are the only mechanism that would actually solve this.
Prioritizing the fix for Root Cause 1 (mounting settings.json into the sandbox) would unblock this entire class of use cases.
Another +1. Same exact use case.
Another +1. Same exact use case.
+1 - there are so many use cases to apply and to make Cowork a greater companion for all.
Another +1. Same exact use case.
+1 - same issue. Beyond enterprise managed-settings concerns, this means any third-party integration that relies on UserPromptSubmit is non-functional for desktop-app users today. The docs at https://code.claude.com/docs/en/desktop-quickstart state that desktop "shares configuration (CLAUDE.md files, MCP servers, hooks, skills, and settings)" - which set expectations that don't match the current Cowork behavior. A footnote in the hooks reference clarifying the Cowork limitation would help vendors set correct customer expectations until the underlying mount/path issue is fixed.
+1 Same use case here. We need to be able to reliably use these hooks in enterprise deployments. Is there any update on this?
+1 same use case, we need this for org level deployment.
+1 for same use cases that hooks within cc are needed
+1 confirming this bug on Claude Cowork Windows as well —
~/.claude/settings.jsonuser-scoped hooks never fire inside Cowork sessions on the Windows host, mirroring the macOS reproduction in the issue body.Environment
cowork_settings.jsonreferenced byWK5() / getSettingsFilename()does not exist in the sandbox; user's actualC:\Users\<user>\.claude\settings.jsonon the Windows host is never mounted into the VMArchitectural impact (Empire trading project, multi-PC workflow)
This bug materially constrains what teams can deploy at user-scope. We discovered #40495 during S69 deep research while designing an internal ADR (ADR-022 —
M4.4 hooks USER-GLOBAL) that would have added Empire-grade safety hooks at~/.claude/settings.json:Net architectural cost: defense-in-depth ONLY active in Code CLI (where Luke runs git/commit/push), Cowork sandbox bypasses entirely. This is workable for our threat model (Cowork is draft/review surface, not destructive), but unexpected for users assuming "user-scope = always fires".
Cross-cluster — broader Cowork hook subsystem issue?
| Issue | Hook scope | Cowork-Windows status |
|---|---|---|
| #40495 (this) | user-scope (
~/.claude/settings.json) | ❌ entirely ignored || #22449 / #39550 family | plugin-scope (
${CLAUDE_PLUGIN_ROOT}PostToolUse) | ⚠️ fires but with literal-as-dirname expansion bug (Cowork-Windows variant — separate comment filed) || #27398 | plugin hooks
--setting-sources userexclusion | OPEN || #18610 | Plugin hooks Windows
/bin/bashpath | OPEN || #21468 | Plugin SessionStart Windows | OPEN |
| #38699 | local marketplace plugins inconsistent | OPEN |
Suggests Cowork's settings resolution + sandbox mount + variable substitution form a single subsystem that diverges from native Claude Code CLI semantics across user-scope, plugin-scope, and platform-specific paths.
Documentation gap suggestion
If user-scope hooks are intentionally bypassed in Cowork (sandbox isolation by design), the docs should state this explicitly — currently
~/.claude/settings.jsonis documented as the canonical user-config location without Cowork qualifier. Users designing safety architectures need to know upfront which scopes are valid in which environments.Resolution suggestions (priority order)
~/.claude/settings.json(and which features that affects)~/.claude/settings.jsonread-only into Cowork sandbox so user hooks DO fire (matches user expectation, opt-in via Cowork settings UI)Empire reproducibility: architectural impact confirmed across 6 sessions. Happy to share ADR-022 design doc + scope-restriction rationale if useful as a real-world architectural workaround case study.
The three root causes you articulate (user settings file absent in sandbox, managed-settings unreachable, env vars unset) match the pattern I've been tracking across a separate cluster of 195 Cowork-shape issues filed in the 14 days from 2026-05-14 to 2026-05-28 (cluster framing notes in cc-safe-setup/docs/cluster-tracker.html for context, not a sales pitch — the tracker is a free reference).
The hook-side defense surface is structurally narrow because, as you note, hooks defined in
~/.claude/settings.jsoncannot fire inside the Cowork sandbox itself. What is reachable is the CLI on the same machine — operators who use both CLI and Cowork can get an automated reminder, on the CLI side, that captures the standing instructions and surfaces them to the operator before they switch to Cowork.I shipped three CLI-side hooks today (2026-05-28) that cover the three most concrete sub-shapes I could verify:
cowork-claude-md-load-checker— SessionStart hook that surfaces~/.claude/CLAUDE.mdat every CLI session start with an explicit Cowork-side reminder block. Directly addresses #62859 (the CLAUDE.md sub-case of the broader pattern you describe).cowork-fuse-staleness-watcher— PreToolUse Bash matcher hook for the FUSE mount silent-corruption variant (#62932 P1).cowork-model-picker-advisor— SessionStart hook for the silent Sonnet 4.6 [1M] default in the Cowork picker (#62949).All three are advisory-only (always
exit 0), file-skip silently when not applicable, and ship with their respective test files (20 / 25 / 25 cases passing). A pre-existingcowork-claudemd-helper.shstandalone script (#403, 40 tests) covers the case where the operator wants to paste CLAUDE.md content directly into a Cowork chat.The standing recommendation that emerges from the cluster framing: until Cowork resolves the settings-resolution gap, treat Cowork sessions as a separate trust boundary from CLI sessions and rebuild the operator's intent (CLAUDE.md, managed policies, env settings) explicitly on the Cowork side at the start of each session. The CLI-side hooks above make that operator-intent visible at the CLI/Cowork boundary so it's at least obvious when the gap matters.
If anyone here wants the four artifacts as a single zero-install reference instead of the repo, I can put them in a Gist — let me know which form is most useful to you.
create hooks inside the plugin, and then upload that plugin into cowork. This worked for me.
Not a maintainer — your root-cause breakdown (sandbox
CLAUDE_CONFIG_DIR, the missingcowork_settings.json, theprocess.platformmanaged-path mismatch) is more precise than anything I can add at the code level. One operator-facing angle that hasn't been called out in the thread, because it's worse than lost convenience:**This is a silent safety failure, not just a missing-automation one. The thread's examples are mostly SessionStart automation (mount Drive, restore context). But the same resolution gap means any PreToolUse safety guard** silently doesn't fire in Cowork either — the hooks people use to block
rm -rf/git push --force/ production-DB writes /git reset --hard. So an operator who has carefully set those up will run a Cowork session believing they're protected, and the protection isn't there. The failure is invisible until a destructive call that "should have been blocked" goes through. A guard that's silently inert is more dangerous than no guard, because it removes the caution you'd otherwise keep.Operator mitigation until this is fixed:
~/.claude/settings.jsonsafety rules apply there.claudeCLI, where user hooks do resolve and fire — not from the Cowork tab.PreToolUse(orSessionStart) hook that appends a timestamp to a log file, then compare a CLI session against a Cowork session. If the Cowork run never writes the line, your guards are inert there — which matches the resolution bug you traced.The enforcement-engine and compliance impact others raised is the same root issue seen from the policy side: if managed settings don't resolve in the sandbox, nothing server-pushed is actually enforced on Cowork activity, so an audit that assumes managed policies are in effect is reporting a state that isn't real.
@Saurav-Sutaria
thanks for posting that — we tried to reproduce it and couldn't, so hoping you can help pin down what's different.
Our test: a minimal plugin (.claude-plugin/plugin.json + hooks/hooks.json) with a single UserPromptSubmit command hook that unconditionally returns {"decision":"block","reason":"..."} — no network, no script, just an inline printf. Uploaded via Customize → Plugins, confirmed enabled, started a fresh Cowork session, typed hi → Claude answered normally, the hook never fired. Same result with a real curl-based hook.
Could you share:
A copy of your hooks.json (redacted) would be hugely helpful. Trying to figure out whether this is a version thing or a config difference. Thanks!
Same symptom on Windows, which may mean this is not limited to the macOS / Linux-sandbox path described in the original report.
Environment
claude --versionis not a recognized command in the user's own PowerShell), so nothing else could be applying these settings.What I observed (tested 2026-08-03)
PreToolUse hooks configured in
~/.claude/settings.jsonnever fire:Writeof a file containingapi_key = "sk-..."→ the hook should returndeny; the file was created with no prompt.permissionDecision: "deny", so the hook script itself works..claude/hooks/...→ the hook should returnask; it ran with no prompt.WebSearchwhose query containedapi_key=...→ shoulddeny; the search ran.I then created the same hook configuration as a project-level
<project>/.claude/settings.jsonand retested in a fresh conversation — all of the above still passed through unblocked. So neither user settings nor project settings are applied.One detail that may narrow this down: on this machine PowerShell commands execute against the real Windows filesystem (they read and write real files on local and removable drives), so there is no Linux VM sandbox involved here. That suggests either the settings-resolution failure is broader than the platform-mismatch explanation, or a second code path leads to the same result.
Happy to run any specific check that would help.
I want to separately flag that Cowork cloud sessions do not respect managed settings hooks. Cowork Desktop ( running locally ) does respect managed settings hooks set via the organization settings under managed settings in Claude code (i.e., server managed settings), but these stop working in Cowork cloud mode which is currently in beta.
See screenshot below for Claude's own investigation from within its cloud sandbox.
<img width="767" height="640" alt="Image" src="https://github.com/user-attachments/assets/8f87f4f7-1dea-4585-bda3-1a4a30422480" />
I wish we could get an understanding of how important is this for Anthropic to fix this. We really rely on this.
Adding operational data from a Windows x64 Cowork deployment, since this issue is still open with no maintainer response and the impact is larger than "hooks do not fire".
Scale of what silently does not run. We maintain 50+ output-validation layers across two shell gates (a response scanner and a pre-send grep) plus ~20 Python validators. Every one of them is correct, tested, and optional at the moment it matters, because nothing in Cowork can invoke them on the model's own output. There is no PreToolUse to block on and no response-time hook to run them from. The gates only run when the model chooses to run them.
What that costs, measured rather than estimated. On 2026-08-13 a session produced a routine status reply with the validator directory already mounted and the scanner one command away. It did not run it. Running the same scanner on that reply afterwards produced four findings in under two seconds: a blocking inbox-aging error, an unestimated-deferral warning on a self-contradicting line, an unread-briefs warning, and three style violations. All four were catchable before send, by tooling that was present, working, and reachable. This is not a one-off; our own logs record the same shape on 2026-07-01, twice in one day.
Our workaround, and why it is only half a fix. Both gates now append to a JSONL ledger on every run, and a checker reports days with substantive work and zero gate invocations. That makes non-invocation visible after the fact. It cannot make it visible before the send, which is the only moment that matters. A gate that is skipped leaves no trace at the time it is skipped.
On the adjacent request in #63360. A
Stophook would be a real improvement for logging, but it fires after the turn completes, so it can record a bad response and cannot withhold one. For validation the useful primitive is a hook that runs on the drafted response and can block or annotate it before it reaches the user, in the same shapePreToolUsehas for tools.Concrete ask, in priority order.
~/.claude/settings.jsonhooks in Cowork at all, per the three root causes in the original report. Everything else is downstream of this.CLAUDE_CODE_HOST_PLATFORMwhen resolving managed settings rather thanprocess.platform.Happy to supply the gate ledger format or the invocation-checker if useful. Environment: Windows x64 packaged desktop app, OneDrive-backed working tree, Cowork sessions both local and cloud.