Skip to content

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.

  • Keep high-risk actions as ask by default
  • Use deny for actions that are clearly dangerous or unnecessary
  • Use allow only for low-risk, predictable actions
  • Put API keys in auth.yaml or environment variables; do not commit them into project files

Typical permission states:

  • allow: auto-allow
  • ask: require confirmation before execution
  • deny: 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 *": ask

This 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": ask

Rules 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.

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 domains

Matching 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.

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:

  • edit and apply_patch are one file-editing tool family with two model-facing formats (patch is accepted as a legacy alias for apply_patch). A rule for one editor applies to the other editor when the other editor has no explicit same-tool rule. This includes deny: *: allow followed by edit: deny disables both edit and apply_patch, because apply_patch inherits the edit-family denial. Configure both names when you want different behavior per format. For example, edit: allow plus apply_patch: deny disables apply_patch but keeps edit available, so GPT/o-series models fall back to edit; conversely, apply_patch: allow plus edit: deny keeps apply_patch available for non-GPT models that would otherwise prefer edit.

  • apply_patch also subsumes write and delete for the paths inside one patch: an added or updated file is additionally checked against any rule that names write, a deleted file and a move’s source against any rule that names delete. Those rules can only make the decision stricter (deny > ask > allow); a wildcard default such as "*": allow does not leak in. A delete: deny therefore cannot be circumvented by removing files through a patch.

  • handoff and done are treated as control gates. Setting either one to deny hides or disables that workflow. Setting it to allow or ask makes the workflow available; Chord may still show local confirmation at the actual handoff/finish point (for example the loop done confirmation). This means ask is 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.

  • done is the loop workflow’s exit signal: Chord mounts it only while a loop is running, and /loop on is refused with a toast when a rule denies done, so done: deny reserves loop termination for you. See Usage: /loop continuous execution mode.

  • delegate matches its agent_type argument, so a role can restrict delegation to selected SubAgent definitions. For example, the ordered rules below deny every target except reviewer and require confirmation before delegating to tester:

    permission:
    delegate:
    "*": deny
    reviewer: allow
    tester: ask

    Denied 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.

  • delegate also controls the delegation workflow as a group. If it is entirely disabled by an effective wildcard deny, Chord disables SubAgent cancellation via cancel, hides nested delegate/cancel from SubAgents, and limits SubAgent notify to 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.

  • cancel therefore depends on delegate: even if cancel: allow is configured, cancel is denied when delegate is disabled. To allow a role to cancel delegated work, enable both delegate and cancel.

  • question: ask is normalized to allow. The question tool 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_exit matches its action argument: keep (the default when action is omitted) or remove. A rule can therefore gate deletion without blocking leaving — worktree_exit: {remove: deny} refuses a removal, including one that also passes discard_changes: true, while a plain leave still follows the wildcard rule. The incoming action is trimmed and lowercased before matching (Remove matches remove), while rule keys stay case-sensitive — write them in lowercase. worktree_enter and worktree_list have 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_context and done. An allowlist role’s "*": deny neither 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: deny removes it, ask keeps it and confirms each call, allow matches the default. Narrow globs such as compact_* 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: ask rules do not raise a confirmation and deny rules 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, and compact_context. They split into two groups:

    • handoff, delegate, and cancel grant the role a capability it did not otherwise have, so YOLO applies exactly one relaxation: an ask rule passes without raising the shared confirmation dialog. allow stays allowed, deny keeps 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 as builder stays single-agent while YOLO is on, because its own rules deny handoff and delegate.
    • done and compact_context only 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: deny still removes the tool or workflow, and an explicit ask on compact_context still confirms each call.
    • SubAgents evaluate their own rules and inherit the mode at execution time: while YOLO is on, their ask decisions pass without raising the shared confirmation dialog, while their deny rules keep rejecting (a read-only worker keeps its write: 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.

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 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 shell non-interactive for foreground commands and background jobs, but there is no Unix-equivalent setsid/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" or git commit -F file instead of editor-driven git commit
  • For amend flows that should preserve the existing message, use explicit non-editor forms such as git commit --amend --no-edit or git commit --amend -C HEAD
  • Avoid interactive Git patch workflows (git add -p, git commit -p, git stash -p) from shell; 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 / --yes or provide all required options explicitly
  • Use sudo -n when 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 ask or deny by default
  • Use web_fetch patterns 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 allow only 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 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.

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.
  • Whole-file replacement: write does 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: edit and apply_patch check 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.

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.

  • Store API keys in ~/.config/chord/auth.yaml when possible
  • Environment-variable references are also supported
  • Do not put real secrets in example configs, scripts, or project repositories
  • Restrict auth.yaml permissions, for example chmod 600 ~/.config/chord/auth.yaml

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

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
  • In shared repositories or team environments, do not globally allow by default
  • Expose only the minimum necessary Hook and MCP tools