Additional
Permission System
Synced from github.com/CoWork-OS/CoWork-OS/docs
CoWork OS uses a layered permission engine instead of a single risk-only approval gate. The current model separates coarse capability gates from export-sensitive approvals so ordinary network reads, raw outbound requests, and provenance-aware prompts can be handled differently without flattening everything into one approval bucket.
Access Profiles are the canonical task-level policy. They replace the need for a separate shell enable/disable choice by carrying sandbox, approval, reviewer, network, filesystem, and domain boundaries together.
What The Engine Decides
Each tool request resolves to one of three outcomes:
allow- execute immediatelydeny- block executionask- prompt the user for approval
The decision includes:
- an exact reason code
- the matched rule or mode
- the scope that matched
- optional persistence targets for workspace or profile rules
Evaluation Order
The runtime first resolves and applies the task's access profile. Individual tool requests then run through this order:
- hard task restrictions and explicit task denylists
- hard guardrails and dangerous-command blocks
- coarse workspace capability gates
- workspace policy script results
- explicit permission rules
- effective access-profile boundaries (legacy mode defaults only for legacy tasks)
- interactive exception routing, or denial when approval is unavailable
Later stages never override earlier hard blocks.
Boundary authorization
Named profiles are evaluated directly. A workspace mutation is not, by itself,
an approval requirement. Granted writes, document generation, and routine commands
inside an available enforced sandbox proceed without an approval lifecycle.
AgentDaemon.authorizeToolAction coordinates tool-specific authorization with the
same permission engine; only real exceptions reach requestApproval.
approval: "never" never creates a harness approval request. An explicit ask rule,
missing consent, or additional scope instead produces a denial unless authority
is already validly granted. Mandatory OS permission dialogs are separate.
Pending approvals are revalidated against their operation and current policy;
argument changes or revoked authority cannot reuse the old grant.
The local runtime does not render the legacy approval queue by default. An
allow decision executes silently. When policy still returns ask—including
network/on-request access, credential use, data export, MCP or other external
side effects, eligible outside-workspace paths, or an explicit
allowAutoApprove: false request—the daemon emits an assistant message and a
durable inline input card with Deny and Allow once. It does not create an
approval_requested row or open a modal. Hard denials, administrator policy,
protected operating-system paths, and approval: "never" remain fail-closed;
automated tasks without a human-input channel are denied. Set
COWORK_APPROVAL_PROMPTS=on before launch only to restore the legacy queue for
diagnostics. Pending approval rows and assistant approval cards fail closed on
restart rather than resuming an unconfirmed action.
Rule Sources
Permission rules can come from several places:
session- temporary grants and session-local rules stored inSessionRuntimeworkspace_db- workspace-local rules stored in SQLiteworkspace_manifest- checked-in workspace policy file at.cowork/policy/permissions.json(mirror only; see Manifest trust — itsallowrules are not trusted on their own)profile- encrypted profile-level rules in secure settingslegacy_guardrails- compatibility rules derived from older trusted-command patternslegacy_builtin_settings- compatibility rules derived from earlier settings models
When rules overlap, the more specific rule wins first. If specificity ties, the source priority is:
session > workspace_db > workspace_manifest > profile > legacy_* > mode
If source priority also ties, the effect priority is:
deny > ask > allow
Scope Types
The engine supports five explicit rule scopes:
tool- match a single tool by namedomain- match a destination domain, optionally scoped to one tool such asweb_fetchorhttp_requestpath- match a tool and absolute path prefixcommand_prefix- match a normalized shell command prefixmcp_server- match a specific MCP server backend
Domain names are normalized to lowercase hostnames. Path scopes are normalized to absolute paths. Command prefixes are whitespace-normalized before comparison. MCP server names are normalized for stable matching.
Location Access
get_current_location uses a dedicated location_access approval type that is separate from
other tool scopes. Location access:
- always requires explicit one-time consent through the operating system's native permission flow; this is OS consent, not the CoWork approval modal or an assistant approval card
- cannot be auto-approved by any permission mode, including
dont_askandbypass_permissions - cannot be persisted as a session, workspace, or profile rule
- delegates the actual permission grant to the operating system's native location dialog
Legacy Permission Modes
Older tasks and API callers may still supply a permission mode. It remains supported as a compatibility input, but new task-level access should use Access Profiles. These compatibility behaviors apply only to legacy authority. New named profiles use boundary authorization above. Legacy behavior depends on the selected mode:
default- allow safe reads, ask on writes, deletes, shell, data export, external services, and side-effecting MCP toolsplan- allow read-only tools, deny mutating and external tools by defaultaccept_edits- allow in-workspace file edits and reads, ask on deletes, shell, data export, network side effects, and external servicesdangerous_only- allow safe reads, in-workspace edits, and a conservative read/test shell allowlist; ask on deletes, browser/system/computer actions,run_applescript, data export, MCP tools, external side effects, and ambiguous shell commandsdont_ask- allow anything not hard-blocked or explicitly denied, exceptdata_export, which still promptsbypass_permissions- skip prompts and ask-rules for normal actions, but still enforce hard guardrails, task restrictions, workspace capability gates, explicit deny rules, and explicitdata_exportprompts
dangerous_only is intentionally conservative for command-tool access. Known read/test commands can run without a prompt, but composite shell expressions and commands with unclear side effects still pause for approval.
dont_ask and bypass_permissions are no longer wildcard escape hatches for outbound transfer.
If the request is classified as data_export, the engine switches back to an explicit approval.
What counts as a mutating tool
plan denying "mutating" tools and default asking on "writes" both depend on
classifying each tool. That classification derives from the canonical taxonomy in
src/shared/types.ts — TOOL_GROUPS["group:write"] and
["group:destructive"] — via isCanonicalWriteToolName, which both
PermissionEngine and tool-policy-engine consult first before falling back to
name-prefix heuristics.
Adding a tool to group:write or group:destructive is therefore sufficient to
make every layer treat it as mutating. A regression test asserts that every
member of those groups classifies as mutating, because the previous
hand-maintained tables had drifted and let several write tools through as
read-only.
Trusted command patterns
When "auto-approve trusted commands" is enabled, a pattern such as echo *
matches a single command only. Any command line containing a shell control
operator — ;, &&, ||, |, backticks, $(…), ${…}, redirection, or a
newline — is ineligible for pattern trust and prompts instead, so a trusted
prefix cannot carry an unrelated chained command. Trusted commands are also
subject to the same auto-approval safety check as the general auto-approve path,
so a command bearing rm or sudo does not auto-approve on a prefix match.
Approvals are briefly reusable for a repeated command. That reuse normalizes
arguments so a batch over many files takes one approval, except when the
command chains/pipes or its executable is an interpreter (sh, bash, python,
node, sudo, env, osascript, …). For those, the whole command line is the
key, so approving one interpreter invocation never covers a different one.
Access Profiles
The main composer exposes four access choices modeled on Codex's access selector. The complete operator and developer contract is in Access Profiles. The short version is: an access profile combines the process sandbox boundary, approval behavior, reviewer behavior, network boundary, and optional filesystem/domain scope. The reviewer can reduce friction, but it cannot widen the sandbox or override an explicit deny rule.
| Access choice | Sandbox | Approval/reviewer | Network |
|---|---|---|---|
| Ask for approval | Workspace-write | On request / user | On request |
| Approve for me | Workspace-write | On request / automatic safety review | On request |
| Full access | Danger-full-access | No approval / no reviewer | Enabled |
| Custom | Named profile | Named profile | Named profile |
Custom profiles are saved in the encrypted permission settings store rather than a plain-text
config.toml. They can define sandbox, approval, reviewer, network, additional roots, filesystem
read/write/deny rules, and domain allow/deny rules. Command tools follow the selected profile mode;
there is no separate shell toggle. A legacy shellAccess: false value is retained only as a
backward-compatible deny for profiles saved by older versions. Deny rules are authoritative and
are applied before legacy workspace permissions. A custom profile that cannot be represented by a
backend fails closed instead of silently widening access.
Sandboxing and approval are separate enforcement boundaries. The sandbox limits what a spawned
process can reach; approval controls whether the action may proceed. macOS and Docker provide a
coarse network on/off boundary, so arbitrary shell or code execution with domain-scoped network
rules is denied unless a domain-aware proxy is available. Built-in network tools can still apply
their domain rules in-process. CoWork also keeps delete separate from write as an additional
destructive-operation gate, even though some sandbox systems group those filesystem operations.
The selected profile is carried through desktop task creation, follow-ups, the local CLI
(cowork run --access-profile <id>), remote Control Plane task creation, task resume, and child
task creation. Child tasks cannot select a profile that widens their parent's sandbox, approval,
network, or scoped-rule boundary. Existing tasks that only have a legacy permissionMode keep
that mode until the user explicitly selects an access profile.
If a named profile is missing, invalid, or cannot be represented by the active sandbox backend, the
runtime uses an unavailable read-only profile and pauses the task with an
access_profile_unavailable reason. No later rule, approval, or legacy mode can widen that state.
Core Automation Defaults
Always-on core automation does not rely on bypass_permissions by default.
Instead, core-created automated tasks inherit an autonomy policy that:
- disables interactive user-input pauses
- seeds an explicit allowlist of automation-safe approval types instead of using a wildcard
- auto-approves
run_commandby default for deep-work style automation - can also auto-approve
network_accessandexternal_servicefor trusted operator work when the task or operator policy opts in - does not auto-approve
data_export - still preserves hard guardrails, workspace capability denials, and explicit deny rules
Core automation also remains beneath the task's effective access profile. Its auto-approval allowlist can reduce interruption for permitted actions, but it cannot add command, filesystem, network, or export capability that the profile or administrator policy removed.
This is the default posture for the Workflow Intelligence core runtime because it keeps routine operator work flowing without turning the permission system off.
In other words:
- core automation uses stronger allow rules
- it does not skip the permission engine entirely
An explicit empty autoApproveTypes list is also preserved as empty. CoWork no longer silently
falls back to "approve everything" when a policy intended to be narrow.
Export-Sensitive Approvals
CoWork now treats outbound data movement as its own approval lane: data_export.
Requests enter that lane when a tool could send workspace or imported content to an external service, even if the tool looks read-like from the user perspective.
Current examples:
http_requestwith a mutating method or request payload is treated asdata_export- plain
http_requestonly stays innetwork_accesswhen it is a simpleGETorHEADwith no body and no custom headers analyze_imageis treated as export because image bytes are sent to an external vision modelread_pdf_visualis treated as export because PDF page images are sent to an external vision model
Ordinary uploaded-PDF reading uses parse_document, which is a local read/extraction path. It is still subject to workspace file permissions and untrusted-content banners, but it is not the visual data-export path unless the task escalates to read_pdf_visual or another outbound tool.
By contrast, Chronicle's screen_context_resolve is a local screen-context lookup. It can read from the local passive Chronicle buffer and optionally capture a fresh local screenshot, but it does not itself export screenshot bytes to an external provider. If the task later sends an image to a provider through analyze_image or another export-sensitive path, that later step still enters the normal data_export approval lane.
This is separate from the coarse workspace network capability:
network: falsestill blocks all network-capable and export-capable tools at the workspace boundarynetwork: trueonly means those tools are eligible to run; export-sensitive requests may still pause for approval- domain-scoped rules can narrow
web_fetchandhttp_requestto approved destinations without globally opening network egress
The session-level "Approve all" convenience toggle also stays narrow. It can auto-approve
run_command and network_access, but not data_export.
Denial Fallback
Soft denials are not always the end of the story.
CoWork tracks denial counts per permission fingerprint in SessionRuntime:
3consecutive soft denials trigger fallback escalation20total soft denials trigger fallback escalation
When a fingerprint crosses the threshold, the next evaluation can surface a direct prompt instead of silently repeating the same denial path.
Hard denials from guardrails, workspace capability gates, and explicit deny rules are never overridden by fallback.
Persistence Destinations
Different rule sources persist in different places:
- session rules and temporary grants live in
SessionRuntimesnapshots - workspace-local rules are stored in SQLite and mirrored to
.cowork/policy/permissions.json - profile rules are stored in encrypted secure settings
Workspace-local rule removal updates both the database row and the manifest mirror. If the manifest write fails, the database removal still succeeds and the UI reports the partial state.
Manifest trust
.cowork/policy/permissions.json lives inside the workspace, so anything with workspace write
access can author it — including the agent, and including a repository that was merely cloned.
It is therefore treated as untrusted input rather than as a source of authority:
denyandaskrules from the manifest apply immediately. They can only narrow access, so a hostile author gains nothing by adding them.allowrules apply only when the workspace database already contains the same rule (same effect and scope). That row is written by the approval flow at the moment the user chooses to persist a grant, so a rule the user actually approved on this machine always matches.
The practical effect for shared repositories is that a teammate's allow rules do not take
effect on first clone. The teammate approves the action once, which creates their own database
row, and from then on the mirror matches. Ignored grants are logged once per workspace.
The enforcement point is filterTrustedManifestRules in
src/electron/security/workspace-permission-manifest.ts, applied where the daemon assembles the
rule set. Separately, .cowork/policy/** is a protected path: file tools may read it but never
write or delete it, so the agent cannot author or remove policy files at all.
User Surfaces
Users can manage permission state from two places:
- the optional legacy approval queue (
COWORK_APPROVAL_PROMPTS=on), which can create one-shot or persisted rules - inline assistant decision cards for interactive exceptions in the normal runtime
- the main composer access selector, which chooses an access profile for the next task or follow-up
- Settings > System & Security, which manages default profiles, custom profiles, profile rules, and workspace-local rules
The workspace-rule panel lets users browse and remove workspace-local rules directly without having to wait for another approval prompt.
Provenance-Aware Approval Context
Approval prompts now include security context when the action could move data outward.
The current flow is:
- imported files are recorded as
user_imported_external - drag-and-drop or clipboard-backed files are recorded as
clipboard_or_drag_data - gateway and channel attachments are recorded as
channel_attachment - reads from those files prepend an explicit untrusted-content banner
- the task runtime tracks recent sensitive or untrusted sources read during the session
- export approvals surface the destination plus the direct source and recent-read hints when available
This keeps rich features intact, but makes it much harder for hidden instructions in imported content to silently trigger an outbound action.
Runtime Integration
SessionRuntime owns the session-local permission state and snapshot persistence.
TaskExecutor delegates permission decisions to the runtime and keeps only task bootstrap,
finalization, and UI projection responsibilities.
The always-on automation runtime layers on top of this by spawning tasks with explicit autonomy presets rather than depending on manual approval toggles.