Permissions & Safety
Chord is a coding agent that can read files, modify files, execute commands, and call external tools. Inspect actions before approving them. Permission rules control risk; they are not an operating-system sandbox.
How to use this page
Section titled “How to use this page”- How rules are evaluated: Principles, Permission model.
- Before allowing a command: Shell / shell risk;
shellis the broadest capability you can grant. - Before allowing writes: File modification risk.
- Credentials and remote surfaces: Credentials and config, Headless boundary, Network and external integrations.
Principles
Section titled “Principles”- Keep high-risk actions as
askby default - Use
denyfor actions that are clearly dangerous or unnecessary - Use
allowonly for low-risk, predictable actions - Put API keys in
auth.yamlor environment variables; do not commit them into project files
Permission model
Section titled “Permission model”Typical permission states:
allow: auto-allowask: require confirmation before executiondeny: reject directly
Rules are keyed by tool name; the full list of built-in tool names is in Built-in tools.
A rule that names a nonexistent tool matches nothing, so a typo silently leaves that tool unmatched instead of failing. Background work runs through shell (with run_in_background: true) plus the job_output, job_list, and job_kill tools.
In the TUI confirmation dialog, V opens the full tool arguments in a read-only viewer, including entries hidden by the summary preview. Close the viewer to return to the pending confirmation; viewing does not approve or change the call. E edits the arguments.
In the TUI confirmation dialog, M opens the add-rule picker for the current tool call; press Enter in that picker to save the selected rule and allow the current call. For delete, the picker suggests reusable parent-directory rules instead of one-off exact-file rules, written in the same spelling permission matching uses, so the saved rule also applies to the same file from another checkout. Directories covering more paths that still need approval appear first, * (any delete path) is always available, and ** (any path in the repository) is also available when every requested path lies inside the repository. The broad ** and * choices are never selected by default.
Permissions can be defined in Agent config. Start with this recommended personal-development template, then tighten or relax it for your project’s risk profile:
permission: "*": allow handoff: deny delegate: deny delete: ask web_fetch: "localhost:8000": ask "169.254.0.0/16": deny "10.0.0.0/8": deny "192.168.0.0/16": deny shell: "sudo *": ask "rm *": ask "rmdir *": ask "mv *": ask "git add *": ask "git checkout *": ask "git clean *": ask "git commit *": ask "git push *": ask "git reset *": ask "git restore *": ask "git tag *": askThis means: allow most tools by default; disable handoff and delegate; require confirmation for file deletion, selected WebFetch URL patterns, and common high-risk shell/git commands. Permission rules use “last match wins”, so the more specific web_fetch and shell rules above override the top-level "*": allow. This is reasonable for a single-user trusted workspace; shared repositories, team services, or automated headless deployments should tighten it further.
This page starts from "*": allow as a trusted-workspace baseline; for a least-privilege baseline instead, the builder agent in Configuration: Agent config starts from "*": deny and opts in only to the tools a role needs.
Permission matching examines the tool call and the repository the session works in. For shell, only the command string is matched: a workdir argument does not participate. For file tools (read, write, edit, apply_patch, delete, view_image), the target path is normalized before rules are matched: a path inside any checkout of the current repository becomes repository-relative (src/main.go), so foo.go, ./foo.go, internal/../foo.go, and an absolute spelling of the same file all hit the same rule no matter which checkout or subdirectory the session runs in. Paths outside every checkout stay absolute. When no repository resolves (a plain directory, or no git binary), the session working directory takes the repository’s place.
File-tool rule patterns are scoped by their form:
*matches every path spelling, meaning “any path”.- A relative pattern (
**,src/**,tmp/*) only matches normalized relative paths, i.e. paths inside the scope described above.**means “any path in the repository”, covering every checkout; when no repository resolves it means “any path under the working directory”../**is accepted as the same thing. - An absolute pattern (
/Users/me/other/**,~/other/**,/**) only matches paths outside every checkout./**means “every absolute path”; combining it with**covers the same ground as*. On Windows, home-relative patterns accept either separator (~\other\**and~/other/**). - An absolute rule does not match a file inside the repository; write the in-repository rule in relative form instead.
This configuration lets the agent delete anything inside the repository and the system temp directory, while .git, .chord, and any AGENTS.md still need confirmation:
permission: delete: "*": ask "**": allow "/tmp/*": allow "/private/tmp/*": allow ".git": ask ".git/*": ask "**/.git": ask "**/.git/*": ask ".chord": ask ".chord/*": ask "**/.chord": ask "**/.chord/*": ask "AGENTS.md": ask "**/AGENTS.md": askRules use last-match-wins ordering, so the broad ** allow comes first and the narrower rules after it bring the protected paths back to confirmation. Outside the repository a path keeps the spelling the caller used, so /tmp/* and /private/tmp/* are both listed: on macOS /tmp is a symlink to /private/tmp, and only the matching spelling hits the rule.
Shell rules only constrain the submitted command string: they do not sandbox the command’s filesystem effects, and an allowed command can still cd elsewhere, invoke another program, or act on an absolute path. Use narrow shell patterns for approval policy and an OS-level sandbox when actual filesystem confinement is required.
WebFetch target matching
Section titled “WebFetch target matching”web_fetch rule patterns use network-aware host matching. A pattern has the
shape host[:port]:
- host: a domain (
example.com), a domain wildcard (*.internal,*), a literal IP (127.0.0.1,::1), or a CIDR range (10.0.0.0/8,169.254.0.0/16,fd00::/8). Bracket IPv6 addresses and IPv6 CIDRs when specifying a port, for example[fd00::/8]:443. - port: omit it (or use
*) for any; use a single port (8080) or a range (8000-9000). When the requested URL omits its port, the port defaults from the URL scheme (http→80, https→443). - Scheme and path patterns are not supported.
web_fetch: "*": allow # default: allow everything "0.0.0.0/8": deny "10.0.0.0/8": deny "127.0.0.0/8": deny "169.254.0.0/16": deny # cloud metadata endpoint "192.168.0.0/16": deny "*:8000-9000": ask # any host on these ports needs confirmation "*.internal": deny # internal domainsMatching happens before the request is sent, against the URL the model supplied. It is not re-checked against the resolved connection IP, so a hostname that resolves to an internal address, or an HTTP redirect to one, is not blocked by an IP/CIDR rule. Treat these rules as intent-level gating, not a network sandbox.
Special permission semantics
Section titled “Special permission semantics”Most tools use the literal allow / ask / deny meaning above, but a few orchestration tools intentionally have extra coupling so permission settings match the workflow Chord can safely run:
-
editandapply_patchare one file-editing tool family with two model-facing formats (patchis accepted as a legacy alias forapply_patch). A rule for one editor applies to the other editor when the other editor has no explicit same-tool rule. This includesdeny:*: allowfollowed byedit: denydisables botheditandapply_patch, becauseapply_patchinherits the edit-family denial. Configure both names when you want different behavior per format. For example,edit: allowplusapply_patch: denydisablesapply_patchbut keepseditavailable, so GPT/o-series models fall back toedit; conversely,apply_patch: allowplusedit: denykeepsapply_patchavailable for non-GPT models that would otherwise preferedit. -
apply_patchalso subsumeswriteanddeletefor the paths inside one patch: an added or updated file is additionally checked against any rule that nameswrite, a deleted file and a move’s source against any rule that namesdelete. Those rules can only make the decision stricter (deny>ask>allow); a wildcard default such as"*": allowdoes not leak in. Adelete: denytherefore cannot be circumvented by removing files through a patch. -
handoffanddoneare treated as control gates. Setting either one todenyhides or disables that workflow. Setting it toalloworaskmakes the workflow available; Chord may still show local confirmation at the actual handoff/finish point (for example the loopdoneconfirmation). This meansaskis not a second, stronger workflow mode for these tools: it mainly keeps the tool visible/available while preserving Chord’s built-in confirmation gate. The trade-off avoids confusing the model with an available control tool that is later impossible to complete, while still preventing silent role switches or premature loop exits. -
doneis the loop workflow’s exit signal: Chord mounts it only while a loop is running, and/loop onis refused with a toast when a rule deniesdone, sodone: denyreserves loop termination for you. See Usage:/loopcontinuous execution mode. -
delegatematches itsagent_typeargument, so a role can restrict delegation to selected SubAgent definitions. For example, the ordered rules below deny every target exceptreviewerand require confirmation before delegating totester:permission:delegate:"*": denyreviewer: allowtester: askDenied targets are omitted from the Delegate tool schema and coordination prompt. If every configured SubAgent target is denied, Delegate is hidden. Permission rules use last-match-wins ordering, so put the wildcard fallback before the specific target rules.
-
delegatealso controls the delegation workflow as a group. If it is entirely disabled by an effective wildcarddeny, Chord disables SubAgent cancellation viacancel, hides nesteddelegate/cancelfrom SubAgents, and limits SubAgentnotifyto owner-only follow-up instead of arbitrary target routing. The reason is that cancelling or targeting other delegated tasks is part of managing delegated workstreams; allowing those pieces while disabling delegation would create a partial control plane that can interfere with work the role is not allowed to orchestrate. -
canceltherefore depends ondelegate: even ifcancel: allowis configured,cancelis denied whendelegateis disabled. To allow a role to cancel delegated work, enable bothdelegateandcancel. -
question: askis normalized toallow. Thequestiontool already asks the user a structured question and waits for their answer, so adding a separate permission confirmation before asking the question would create a redundant prompt without reducing the risk of the final decision. -
worktree_exitmatches itsactionargument:keep(the default whenactionis omitted) orremove. A rule can therefore gate deletion without blocking leaving —worktree_exit: {remove: deny}refuses a removal, including one that also passesdiscard_changes: true, while a plain leave still follows the wildcard rule. The incomingactionis trimmed and lowercased before matching (Removematchesremove), while rule keys stay case-sensitive — write them in lowercase.worktree_enterandworktree_listhave no permission-matching argument: their calls are matched as*, so a plain rule for either one applies to every call. -
Two control tools are exempt from wildcard-only rules, because the feature that makes them reachable is itself the authorization:
compact_contextanddone. An allowlist role’s"*": denyneither hides them nor rejects their calls; otherwise enabling model-driven compaction or starting a loop would silently do nothing. Only a rule that names the tool overrides this:denyremoves it,askkeeps it and confirms each call,allowmatches the default. Narrow globs such ascompact_*count as naming it, and so do rules written with an argument pattern, since neither tool takes a permission-matching argument. Neither has an external side effect (one shrinks the context, the other ends a loop), so a wildcard rule has no capability to protect here. -
YOLO removes the confirmation friction of ordinary work (file edits and shell commands) and nothing else. While it is on, the main agent’s ordinary tools skip permission checks entirely:
askrules do not raise a confirmation anddenyrules do not block. The bypass only widens permissions, and switching YOLO off restores the original permissions. Control tools change the agent topology or the session lifecycle rather than the risk of one operation, so under YOLO they keep following their configured rules:handoff,delegate,cancel,done, andcompact_context. They split into two groups:handoff,delegate, andcancelgrant the role a capability it did not otherwise have, so YOLO applies exactly one relaxation: anaskrule passes without raising the shared confirmation dialog.allowstays allowed,denykeeps rejecting, and wildcard defaults behave as they do without YOLO. YOLO therefore never adds an orchestration capability the rules do not already grant: a single-agent role such asbuilderstays single-agent while YOLO is on, because its own rules denyhandoffanddelegate.doneandcompact_contextonly end or shrink the current unit of work, so YOLO leaves their dedicated semantics untouched: each behaves exactly as it does without YOLO, and a rule that names one of them keeps its effect:denystill removes the tool or workflow, and an explicitaskoncompact_contextstill confirms each call.- SubAgents evaluate their own rules and inherit the mode at execution time: while YOLO is on, their
askdecisions pass without raising the shared confirmation dialog, while theirdenyrules keep rejecting (a read-only worker keeps itswrite: deny). Switching YOLO off restores their confirmations on later calls.
Permissions are Agent-level configuration, not a simple global switch.
For shell, a specific allow pattern such as "git *": allow does not auto-allow a command that carries extra work: unquoted shell separators (;, &&, ||, |, &, or newlines), command substitution ($(...) or backticks, including inside double quotes), process substitution (<(...) or >(...), which spawns a subcommand of its own), an environment assignment or declaration (PATH=..., LD_PRELOAD=..., or any other variable, and export/declare/local of the same, whether it prefixes the command or stands on its own line), and a quote the scan cannot resolve (an unterminated quote, or a trailing backslash). Those calls fall through to the next matching rule, typically ask or deny.
Metacharacters that are literal payload (single-quoted, or escaped with a backslash) still match the narrow rule. Use this as a safety backstop, not as shell sandboxing; keep broad rules like shell: allow or shell: { "*": allow } for only fully trusted roles.
A command-specific allow does, however, cover the full capability of that command, including output redirections. If echo * is allowed, then echo secret > ~/.bashrc, echo x >> file, and data > /dev/tcp/host/port are all allowed: the redirection target is part of the same command text, not a separate tool call, so it is not matched or gated on its own. An environment-assignment prefix is different: LD_PRELOAD=./x.so echo hi is matched as the whole LD_PRELOAD=... echo hi text, so it falls through to the next matching rule instead of riding on echo *. A rule written for the command’s own text still decides the outcome: an assignment that keeps a narrow allow from matching cannot push a denied command into a broader rule, nor let a narrow ask fall through to a broad allow.
Grant a command-level allow only to commands whose worst case (arbitrary file writes via redirection) you accept; otherwise keep them at ask.
Read-only shell commands (batching only)
Section titled “Read-only shell commands (batching only)”Chord recognizes some shell commands as provably read-only so they can run in parallel with other reads instead of serializing the turn. The judgment is conservative: any uncertainty means “not read-only”. Pipes and && / || / ; chains count as read-only only when every side is read-only, command / nice unwrap one level, and redirections other than <, <<<, or 2>&1-style fd duplication veto. Unknown flags veto, so a new write option in a future tool version fails closed. rg is read-only only with --no-config: without it, RIPGREP_CONFIG_PATH can add --pre and run an external command. Words the shell would rewrite veto as well: pathname expansion (*, ?, [), backslash quote removal, brace expansion ({...}), and $'...' ANSI-C escapes are all resolved before the command sees its argv and could turn an innocent-looking operand into a flag (a file named -o makes sort * run as sort -o ...). The same characters inside quotes are literal payload and stay allowed.
This classification never changes permissions: an ask / deny rule still wins, and a read-only command still asks when your rules say so. If a read-only command still runs serially, that is the classifier staying conservative, not a bug. If you ever see a write batched alongside other tools, report it as a bug with the exact command string.
Shell / shell risk
Section titled “Shell / shell risk”shell can execute system commands and should be treated carefully. shell is intentionally non-interactive whether the command runs in the foreground or as a background job: Chord does not wire model-controlled stdin into child processes, Unix child processes run without a controlling TTY, and high-confidence interactive commands are rejected before execution.
Plain stdin reads such as shell read/select observe EOF instead of waiting for model input; provide data explicitly with a pipe, here-doc, file, or arguments when a command expects input. Login wizards, terminal editors, pagers/full-screen TUIs, password prompts, and commands that require /dev/tty should be run manually in a real terminal or rewritten with explicit non-interactive input/flags.
Platform notes for shell (foreground or background job):
- On Unix, Chord starts child processes in a new session and cleans up by process group on timeout/cancellation.
- On Windows, Chord still keeps
shellnon-interactive for foreground commands and background jobs, but there is no Unix-equivalentsetsid/process-group control path here; timeout/cancellation cleanup falls back to direct process termination and may be less complete for descendant processes.
Common rewrites:
- Use
git commit -m "message"orgit commit -F fileinstead of editor-drivengit commit - For amend flows that should preserve the existing message, use explicit non-editor forms such as
git commit --amend --no-editorgit commit --amend -C HEAD - Avoid interactive Git patch workflows (
git add -p,git commit -p,git stash -p) fromshell; stage explicit pathspecs or run them manually - Remove TTY allocation flags from container commands (
docker exec -it,docker run -t,podman run -t,kubectl exec -it) unless you are running them manually in a real terminal - Use
npm init -y/--yesor provide all required options explicitly - Use
sudo -nwhen you want sudo to fail non-interactively instead of prompting - Pipe input or use a here-doc when a command truly accepts non-interactive stdin
Recommendations:
- Keep file deletion, bulk rewrites, network downloads, and database operations as
askordenyby default - Use
web_fetchpatterns to gate local/private services or sensitive endpoints: by host/port (web_fetch: { "localhost:8000": ask }) or by address range (web_fetch: { "169.254.0.0/16": deny, "*:8000-9000": ask }) - Set
allowonly for a small set of predictable development commands - Do not treat permission matching as a security sandbox
Important: Chord’s permission matching is product-level risk control, not OS-level isolation or a security sandbox.
File modification risk
Section titled “File modification risk”File tools act directly on the workspace; they are not a dry run. Use Git for important repositories and keep production configuration, deployment scripts, and secret files subject to ask.
Which actions change files
Section titled “Which actions change files”| Action | Effect |
|---|---|
write |
Create a file or replace an entire existing regular file. |
edit / apply_patch |
Change local content; patches can also add, move, and delete files. Independent file groups may succeed partially, so inspect applied changes before retrying. |
delete |
Delete a file without requiring a complete read first. Deleting a symbolic link removes the link, not its target; write refuses to follow symbolic links. |
read / view_image / grep |
Leave files unchanged, but their content can enter the conversation and model requests. Read-only does not prevent sensitive data from leaving the machine. |
Checks and backups before writing
Section titled “Checks and backups before writing”- Whole-file replacement:
writedoes not refuse solely because the model has not read the current version. Unread, partially read, or subsequently changed content is backed up when possible, with the location reported in the result. Content matching the model’s most recent write or edit needs no additional backup. - Unreadable previous content: the write may still proceed without a backup; the result warns about the risk. A new file does not require a previous-version backup.
- Local edits:
editandapply_patchcheck their anchors against the current file. If a changed file still has valid anchors, editing may proceed after a warning, with a best-effort backup of risky non-empty previous content. - Deletion: if the model has not observed the file’s current content, Chord attempts to back up its exact disk bytes, including empty files. Backups do not follow symbolic links.
Backups are limited to 10 per path, 200 per session, 10 MiB per file, and 50 MiB per session. A failed or oversized backup does not stop a local edit, and the failure may appear only in logs. Session backups are not a substitute for version control; cleaning up a session also deletes its backups.
File-reading limits
Section titled “File-reading limits”Path-reading tools reject restricted device paths such as /dev/stdin, /dev/stdout, and /dev/stderr. Local text tools prefer UTF-8 or BOM-marked Unicode, with limited support for encodings such as GB18030, Big5, and Shift-JIS. Ambiguous or unsupported encodings fail with an error. web_fetch decodes the character set declared by the HTTP response.
Credentials and config
Section titled “Credentials and config”- Store API keys in
~/.config/chord/auth.yamlwhen possible - Environment-variable references are also supported
- Do not put real secrets in example configs, scripts, or project repositories
- Restrict
auth.yamlpermissions, for examplechmod 600 ~/.config/chord/auth.yaml
Headless boundary
Section titled “Headless boundary”chord headless is suitable as a lower-level control plane for bots/gateways, but it does not provide multi-tenant isolation, browser security boundaries, or complete permission hosting by itself.
If you connect it to a chat platform, automation system, or team service, enforce additional controls in the outer layer:
- Which working directories may be accessed
- Which commands may be called
- Who can approve high-risk operations
- How events are audited and retained
Network and external integrations
Section titled “Network and external integrations”Chord can integrate with:
- provider APIs
- LSP
- MCP
- Hooks
- local shell commands
Each capability expands the runtime boundary. Before enabling one, confirm:
- Whether you really need it
- Which resources it can read or write
- How to roll back or disable it when it fails
- Whether sensitive data may be sent to an external service
Usage recommendations
Section titled “Usage recommendations”- In shared repositories or team environments, do not globally
allowby default - Expose only the minimum necessary Hook and MCP tools