# Security model in Copilot CLI

Set a developer baseline and a CI baseline for ticket-service in Copilot CLI with --allow-tool and --deny-tool patterns, path and URL permissions, trusted folders, secret redaction and enterprise managed settings.

Source: https://ai-sw-factory.mellicci.dev/fundamentals/security-model/copilot-cli

## At a glance

| | Copilot CLI |
|---|---|
| Term | Permissions (tool, path and URL), trusted folders, permission modes |
| Configured in | `--allow-tool`, `--deny-tool`, `--allow-url`, `--deny-url` flags; `~/.copilot/permissions-config.json` (saved approvals); `~/.copilot/settings.json` (`allowedUrls`, `deniedUrls`); `trustedFolders` in `~/.copilot/config.json`; enterprise `managed-settings.json` |
| Loads / runs | The harness checks every tool call against the rules; a prompt answers "ask" when a person is present, and in `-p` runs an unapproved action is denied |
| Scope & precedence | Deny beats allow, even under `--allow-all`; flags apply to one session only |

## Build the scenario

Two baselines for `ticket-service`. Copilot CLI has no committed per-repository file for tool rules, so the developer baseline is a launch command.

| Step | What you write or run | Where |
|---|---|---|
| 1 | Launch with the developer baseline below | Your terminal, in the repo root |
| 2 | Confirm folder trust when asked; choose "this session" unless the folder is always safe | First-run prompt |
| 3 | Run the CI baseline in `copilot -p` | `.github/workflows/agent-ready.yml` |

Developer baseline. Reads are allowed automatically, other shell commands and URLs prompt:

```shell
copilot \
  --allow-tool='shell(pnpm test), shell(pnpm lint)' \
  --deny-tool='read(.env), read(.env.local)' \
  --deny-tool='shell(rm), shell(git push)' \
  --deny-tool='write(src/db/migrations)'
```

The documentation doesn't specify a pattern for `.env*`, for `git push --force` alone, or for a whole directory in `write(...)`. The command above uses exact paths, except `write(src/db/migrations)`, which may not cover files below that directory, and denies all `rm` and `git push`; the managed rules below do support globs.

CI baseline for the `agent-ready` job (the headless page shows the full workflow):

```shell
copilot -p "$PROMPT" -s --no-ask-user \
  --allow-tool='write, shell(pnpm:*), shell(git add:*), shell(git commit:*)' \
  --deny-tool='shell(git push), shell(rm), shell(curl), shell(wget)' \
  --deny-url=pastebin.com \
  --secret-env-vars=GH_TOKEN \
  --share=agent-run.md
```

| Control | Setting |
|---|---|
| Never | `--allow-all`, `--yolo`, `COPILOT_ALLOW_ALL` |
| Workflow token | `permissions: contents: write, pull-requests: write, issues: read` |
| Who triggers | Only maintainers can apply `agent-ready` (repository label permissions) |
| Issue text | Passed through `env:` variables as data, never pasted into the script |
| Logs | Upload `agent-run.md` and keep the Actions log |
| Sandbox | `--sandbox` exists but is experimental-mode only; the docs don't specify more for Actions |

## Specifics

### Permission modes

| Mode | How to enter | Behavior |
|---|---|---|
| Default | Start normally, `/permissions default` | Reads allowed; edits, shell and URLs prompt |
| Plan | `--plan`, `/plan`, Shift+Tab | Explores, blocked from editing project files; a safety net, not a guarantee |
| Autopilot | `--autopilot`, `--mode autopilot`, Shift+Tab | Keeps working until `task_complete`; with limited permissions it denies whatever needs approval |
| Assisted | `/permissions assisted` | Still prompts, with an LLM safety recommendation |
| Allow-all | `--allow-all`, `--yolo`, `/allow-all` | Tools, paths and URLs all allowed; use only in isolation |

| Prompt answer | Scope |
|---|---|
| `y` / `n` | This request once |
| `!` / `#` | Allow / deny similar requests for the session |
| This location | Saved per Git root or directory in `permissions-config.json` |
| Always | Saved in a config file |

Approving a command for the session allows it "in any way": approving `rm ./this-file.txt` allows `rm -rf ./*`. `/permissions reset` clears session approvals.

### Allow, ask and deny rules

| Kind | Examples | Notes |
|---|---|---|
| `shell` | `shell(git push)`, `shell(git:*)` | `:*` matches the stem plus a space, so `git:*` doesn't match `gitea`; for `git` and `gh`, name the first-level subcommand |
| `write` | `write`, `write(src/*.ts)` | Path form scopes to that path; symlinks and `..` resolved |
| `read` | `read`, `read(.env)` | |
| `url` | `url(github.com)` | Web fetch and network shell commands |
| Server name | `MyMCP(create_issue)`, `MyMCP` | MCP tools |

| Layer | Flag | Effect |
|---|---|---|
| Visibility | `--available-tools`, `--excluded-tools` | Tools the model can see at all; beats `--allow-tool` |
| Permission | `--allow-tool`, `--deny-tool` | Deny beats allow and saved approvals |
| Paths | `--add-dir`, `--allow-all-paths`, `--disallow-temp-dir` | Default: cwd, subdirectories and the temp directory; shell path detection is heuristic |
| URLs | `--allow-url`, `--deny-url`, `allowedUrls`/`deniedUrls` | All URLs prompt by default; deny wins; `http` and `https` approved separately |

Persistent rules: `permissions-config.json` stores only allow approvals (`commands`, `write`, `mcp` and similar) per location. It has no deny or ask rules and no shared repository policy. `.github/copilot/settings.json` is committed and shared, but its supported keys include `deniedUrls`, not tool rules. To share deny rules with a team, put them in a launch script or use managed settings.

### Approvals and headless runs

| Situation | Behavior |
|---|---|
| Interactive | Unapproved actions prompt |
| `copilot -p` | No one answers; an action that isn't pre-approved is denied |
| `--no-ask-user` | The agent doesn't ask clarifying questions |
| Project MCP, repo hooks, extensions in `-p` | Load only in a trusted folder, or when `GITHUB_COPILOT_PROMPT_MODE_*` is `true` |

Trusted folders: the first-run prompt trusts the folder for the session or for future sessions. Permanent trust is the `trustedFolders` array in `config.json`. Trust decides what project configuration loads; it is separate from tool approval.

### Managed and project policy

Enterprise `managed-settings.json` is the place for deny and ask rules.

| Key | Purpose |
|---|---|
| `permissions.deny` / `ask` / `allow` | Precedence deny > ask > allow; selectors `Shell(...)`, `Read(...)`, `Edit(...)`, `Domain(...)` with globs |
| `permissions.disableBypassPermissionsMode` | `"disable"` suppresses `--allow-all`, `--yolo` and `/allow-all` |
| `allowedMcpServers` / `deniedMcpServers` | Match by `serverUrl`, `serverCommand` or `serverName` |
| `sandbox` | `enabled`, `failIfUnavailable`, `allowBypass` set a sandbox floor |

```json
{
  "permissions": {
    "deny": ["Shell(rm -rf *)", "Read(./.env*)", "Edit(/src/db/migrations/**)"],
    "ask": ["Shell(git push *)"],
    "disableBypassPermissionsMode": "disable"
  }
}
```

A managed `deny` blocks the operation for all users, and a managed `ask` can't be satisfied by allow-all or a saved approval. The documentation doesn't say how to obtain managed settings without an enterprise plan.

### Untrusted input and audit

| Topic | Documented |
|---|---|
| Prompt injection | Forked-PR workflows are higher risk; running `copilot` directly gives it broad access to the workflow environment |
| Secrets | `--secret-env-vars=VAR` redacts variables from shell and MCP environments; `GITHUB_TOKEN` and `COPILOT_GITHUB_TOKEN` are redacted by default |
| Session record | `~/.copilot/session-state/<id>/events.jsonl`, plus `session-store.db` |
| Logs | `~/.copilot/logs/process-<timestamp>-<pid>.log`; `--log-dir` moves them |
| Transcript | `--share=PATH` writes Markdown after a `-p` run; `--share-gist` makes a secret gist |
| Synced sessions | On Business and Enterprise, policy "Store local sessions in the Cloud" controls syncing |

## Gotchas

- `--allow-tool='shell(pnpm:*)'` also allows `pnpm exec` and other subcommands. Use `shell(pnpm test)` to allow one.
- Flags don't persist and `permissions-config.json` can't deny. A baseline you want every time needs a wrapper script or managed settings.
- Path and URL checks for shell commands are heuristic: custom variables, obfuscated URLs and complex constructs can slip through. Treat them as guardrails and add a sandbox.
- Choosing "approve for the rest of the session" on `rm` approves every `rm` form.
- Permissive flags may be blocked for Business and Enterprise users by an administrator.
