Date: 2026-05-22 Status: Accepted
daemon needs a conservative permission model — closer to Claude Code than to Pi, which has minimal guardrails. The model must be safe by default, low-friction for routine operations, and configurable without ever allowing truly dangerous operations to bypass safeguards.
| Scope | Default |
|---|---|
| CWD subtree at invocation | Implicitly allowed — no prompt |
| Any path outside CWD subtree | Prompt required |
Path grants are tree-based: approving /Users/rendle/foo implicitly allows all reads under /Users/rendle/foo/. Paths are always normalised (symlinks resolved, .. collapsed) before checking.
No implicit allows anywhere, including within CWD.
Grants are tree-based: approving a path for write/edit/delete covers the entire subtree. Each novel path root requires a prompt.
Two categories, handled differently:
Simple commands — no pipes (|), semicolons (;), &&, ||, command substitution ($(...) or `), or process substitution:
- Stored approvals use glob patterns (e.g.,
git *,dotnet build*) - When prompting, the UI suggests a reasonable pattern alongside the exact command string; the user chooses which to approve
- Stored approvals are checked against the command string before prompting
Composite commands — anything that is not a single simple command per the Bash grammar. Concretely, the command is treated as composite if it contains any of:
- Pipes:
|,|& - Command separators:
;,&&,||, newlines (multi-line scripts), backgrounding& - Command substitution:
$(...), backticks`...` - Process substitution:
<(...),>(...) - Redirects:
>,>>,<,<<(heredoc),<<<(here-string),2>,&>,>& - Subshells / groups:
( ... ),{ ... ; } - Inline environment assignments preceding the command (
FOO=bar cmd)
The classifier runs on the raw string the agent submits — not on a shell-expanded form. If parsing is ambiguous, the command is treated as composite (fail safe).
- Always prompt, regardless of stored approvals
- The entire shell string is the approval unit for "allow once" only
- Composites cannot be stored as persistent approvals — each occurrence requires explicit user confirmation
HTTP calls from tools require per-domain approval, stored at project level only (.dmon/settings.yaml). Domain approvals are not user-global: a domain approved in one project is not approved in another. Subdomains require their own approval (api.example.com ≠ example.com).
A hardcoded denylist of dangerous command patterns is checked before any approval lookup. Matches are rejected unconditionally — no approval level can override them. The denylist covers patterns including but not limited to:
rmtargeting root or system directories (/,/etc,/usr,/bin, etc.)- Disk format and wipe commands (
mkfs,dd if=/dev/zero,shred) - Commands that disable or bypass security mechanisms (
chmod -R 777 /,chattr -i /) - Fork bombs and resource exhaustion patterns
- Anything invoking
sudoorsu(daemon never runs as root and never elevates)
The denylist is not user-configurable.
Every prompt offers four options:
| Option | Scope | Storage |
|---|---|---|
| Allow once | This invocation only | Not stored |
| Allow for project | All sessions in this project | .dmon/settings.yaml |
| Allow globally | All projects | ~/.dmon/settings.yaml |
| Deny | Rejected, reported to agent | Not stored |
Project settings take precedence over global settings. Denying does not store a permanent deny rule — the agent may ask again in a future session.
Within a single scope (project or global), the most-specific rule wins:
- Allows and denies coexist in
settings.yamlunderpermissions.<tier>.allowandpermissions.<tier>.deny. - For path-based tiers (read/write/edit/delete), the longest matching path prefix decides. So
allow: /Users/me/work+deny: /Users/me/work/secretsis valid — reads undersecrets/are blocked while the rest ofwork/is allowed. - For bash globs, an explicit
denypattern beats anallowpattern; ties resolve to deny. - Across scopes, project wins over global (existing rule).
Extension loading is a distinct permission tier with a mandatory multi-step gate — not a simple prompt. The gate is a pipeline that cannot be short-circuited by stored approvals:
-
Source fetch — mandatory. The source for the exact package version being loaded must be retrievable. The mechanism: the
.nupkgis downloaded and its.nuspecis parsed for a<repository url="..." commit="...">element; a non-emptyurlis required. Source files are then fetched at the recorded commit (fromraw.githubusercontent.comfor public GitHub repos without authentication; via theghCLI for private repos). If the nuspec has no<repository>element, or if source cannot be fetched, the load is refused unconditionally — no approval can override this. TheghCLI is not required for public-repo source fetches; it is used only for private repos and for GitHub enrichment signals. -
Source analysis — dmon's LLM performs a security pass over the extension source, inspecting for suspicious patterns:
- Filesystem access outside the CWD subtree
- Outbound network calls (flagged with context — expected for some extension types)
- Process spawning
- Reflection abuse (dynamic assembly loading)
- Credential or environment variable harvesting
- Obfuscated or machine-generated code that resists inspection
The analysis produces a structured report:
risk_level(low/medium/high), a list of findings (empty if clean), and a plain-language summary. The report notes explicitly that it covers the extension's own source only, not transitive NuGet dependencies. -
Report to user — the analysis report is presented in full before any confirmation prompt. The user sees what the analysis found (or that nothing was found) before being asked whether to proceed.
-
Confirmation — standard four-option prompt (allow once / allow for project / allow globally / deny), presented after the report.
Stored approvals for extension versions. "Allow for project" and "allow globally" suppress the confirmation prompt on subsequent loads of the same package-id@version. They do not suppress the source fetch and analysis — analysis always runs on the first load of a given version in a given installation. A patch-version bump resets the approval for that version.
The stored approval key format is "{packageId}@{version}" — for example, "Acme.MyTool@1.2.3". This key is stored under permissions.extensions.allow in the settings file. A change to any part of the version string (major, minor, or patch) produces a different key and therefore requires a new approval. Pre-release suffixes (e.g. 1.0.0-beta.1) are treated as distinct versions.
Why source availability is non-negotiable. The source analysis is the primary safety mechanism for extension loading. An extension whose source cannot be fetched cannot be analysed and cannot be trusted. This is not a configurable behaviour.
Extension loads always carry risk: high regardless of stored approvals.
The risk field on tool.confirmRequest (ADR-003) is set as follows:
| Risk | Conditions |
|---|---|
none |
Read within CWD subtree — no prompt issued |
low |
Read outside CWD; write/edit/delete to an already-approved path; simple bash already in allowlist |
high |
Write/edit/delete to a new path; novel bash command; composite bash command; HTTP call |
Hosts use risk to decide how to present the confirmation. The Avalonia host may show a visual diff for high-risk file operations (planned for V1.5+).
# .dmon/settings.yaml (project) or ~/.dmon/settings.yaml (global)
permissions:
read:
allow:
- /Users/rendle/other-project
write:
allow:
- /Users/rendle/other-project
deny:
- /Users/rendle/other-project/secrets
bash:
allow:
- "git *"
- "dotnet build*"
- "dotnet test*"
- "ls *"
deny:
- "git push --force*"
http:
allow:
- api.github.com
- registry.npmjs.org
extensions:
allow:
- "Acme.MyTool@1.2.3"
- "SomeOrg.Helper@0.9.0"- Safer than Pi by design. No tool executes silently; every novel operation surfaces to the user.
- Low friction for routine operations. Read within CWD requires no interaction. Common bash commands and domains can be pre-approved at project or global level.
- Composite commands always surface. Shell composition cannot be used to smuggle approved command fragments into unapproved operations.
- Denylist is non-negotiable. Genuinely dangerous operations cannot be approved regardless of user preference or configuration.
- HTTP permissions are scoped to projects. Domain approvals don't leak across projects.
- The permission gate sits in the
IChatClientmiddleware pipeline (ADR-002), beforeFunctionInvokingChatClientdispatches tool calls. All tools — built-in and extension — pass through the same gate.