github / github/copilot-cli

`--deny-tool="read(...)"` blocks ALL file reads regardless of pattern; no persistent permission profiles for non-interactive mode

Open
#2,722 0 comments 1 reaction 0 assignees View on GitHub
area:non-interactive area:permissions area:tools
Dominant language
Shell
Stars
11.2k
Forks
1.9k
Avg merge
14h 16m
Merged PRs (30d)
6

Description

### Describe the bug

Any `--deny-tool` pattern targeting `read()` blocks ALL `read()` tool calls, not just files matching the pattern. This makes it impossible to selectively deny reads to sensitive files (e.g., `.env`) while allowing reads to the rest of the project.

Even when the deny pattern is fixed, `read()` deny is bypassed trivially via `shell(cat .env)` or `shell(sed .env)` - there is no way to deny file access at the filesystem level.

Finally, without persistent permission profiles (#307), every non-interactive invocation requires 10+ CLI flags. All three competing AI coding CLIs (Claude Code, Gemini CLI, Codex CLI) provide file-based permission configuration.

### Affected version

GitHub Copilot CLI 1.0.27.

### Steps to reproduce the behavior

### Bug 1: `read()` deny blocks ALL reads

1. Run copilot in non-interactive mode, allowing reads to the entire project but denying reads to `.env` files only:

```bash
copilot -p "Read AGENTS.md and tell me the first heading" \
--available-tools="bash,view,rg,glob" \
--allow-tool="read($(pwd)/**)" \
--deny-tool="read($(pwd)/.env*)" \
--deny-tool=write
```

2. Observe that ALL `read()` calls are denied, not just `.env`:

```
Read AGENTS.md
Permission to run this tool was denied due the following rules:
`read(/home/user/my-project/.env*)`
```

3. Copilot falls back to `shell(sed ...)` to read files, wasting a turn.

We tested four patterns (both `$(pwd)` and absolute paths) - all produce the same result:

| Pattern | Result |
| ----------------------------------------------------- | ---------------- |
| `--deny-tool="read($(pwd)/.env*)"` | Blocks all reads |
| `--deny-tool="read($(pwd)/**/.env*)"` | Blocks all reads |
| `--deny-tool="read(.env*)"` | Blocks all reads |
| `--deny-tool="read(/absolute/path/to/project/.env*)"` | Blocks all reads |

### Bug 2: `read()` deny is trivially bypassed via shell

1. Run copilot with `read()` deny for `.env`:

```bash
copilot -p "What keys are in the .env file?" \
--available-tools="bash,view,rg,glob" \
--allow-tool="read($(pwd)/**)" \
--allow-tool="shell(git diff:*)" \
--deny-tool="read($(pwd)/.env*)" \
--deny-tool=write
```

2. Copilot's `read()` is denied, but it routes around via shell:

```
Read .env
Permission denied due to: read(.env*)

Read .env contents via shell
sed -n '1,20p' .env
1 line...

The .env file contains: [contents displayed]
```

The `read()` deny only blocks the `read()` tool. `shell(cat .env)`, `shell(sed .env)` etc. all work.

### Expected behavior

1. `--deny-tool="read(.env*)"` should deny reads to `.env` files only, not ALL files.
2. File-level deny should apply across all tools - if `.env` is denied, `shell(cat .env)` should also be denied.
3. A persistent settings file (e.g., `~/.copilot/settings.json` or `.copilot/config.toml`) should allow storing permission profiles so that 10+ flags per invocation are not needed.

### Additional context

### Competing CLI tools all solve this with settings files

**Claude Code** ([permissions docs](https://code.claude.com/docs/en/permissions)):

- `~/.claude/settings.json` + `.claude/settings.json` (project-level)
- JSON-based allow/deny/ask rules with glob patterns
- 4-level hierarchy: admin > user > project-local > project-shared
- deny > ask > allow evaluation order

**Gemini CLI** ([policy engine docs](https://geminicli.com/docs/reference/policy-engine/)):

- `~/.gemini/policies/*.toml` (TOML-based policy files)
- Priority system (0-999), `argsPattern` regex matching on tool args
- Workspace + user + admin tiers with deterministic precedence

**Codex CLI** ([config reference docs](https://developers.openai.com/codex/config-reference)):

- `.codex/config.toml` (TOML-based per-project config)
- Per-path filesystem permissions: `"/path" = "read"`, `"/path" = "none"`
- Named permission profiles, sandbox modes (`read-only`, `workspace-write`)
- `:project_roots` token for portable project-scoped access

### Host tool collision with deny-tool strings

When copilot is invoked from another AI coding tool (e.g., Claude Code), `--deny-tool="shell(rm:*)"` causes the host tool's own deny-list to block the entire command, because it pattern-matches the literal text `rm` in the Bash command string. This forces users to build wrapper scripts that hide the deny flags from the host's permission system.

### Workaround

We built a wrapper script that encapsulates all permission flags and is enforced by a validation hook. This works but is a band-aid.

### Related

- #307 (Comprehensive Permissions System Improvements Proposal, closed)

Contributor guide

Open the contributing guide

Research direction

Start by reproducing the three command-line cases in the issue, comparing read() pattern matching with shell(cat/sed) access and the repeated permission flags. Review the related permissions proposal in #307 and the existing wrapper-script workaround before determining the scope. Done means path patterns selectively deny matching files, file restrictions apply across tools, and permission profiles can be reused in non-interactive invocations.

Written by the indexing model from the issue text.

Assessment

Tech stack
shell
Domain
cli, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.