microsoft / microsoft/vscode

`github.copilot.chat.additionalReadAccessPaths` is bypassed when reading in-workspace symlinks pointing outside the workspace

Open
#336,153 0 comments 0 reactions 1 assignee Claimed by @TylerLeonhardt View on GitHub
Dominant language
TypeScript
Stars
193k
Forks
42.4k
PR merge metrics
PR metrics pending

Description

### Environment & System Info
- **VS Code Version:** 1.137.0 (Commit: `645f29cc3176500b4b5762ba887cf2a7f0ffdf2c`, x64)
- **GitHub Copilot Extension Version:** 0.65.0 (`copilot-chat`)
- **OS:** Linux (Ubuntu 22.04 LTS / Linux 6.8.0-1059-azure x86_64) via VS Code Remote - SSH / Remote Containers
- **Client OS:** Windows 11 (VS Code Desktop connecting to remote Linux)

#### List of Installed Extensions (Remote):
- `GitHub.copilot-chat@0.65.0` (built-in Copilot extension)
- `ms-python.python@2026.4.0`
- `ms-python.vscode-pylance@2026.3.1`
- `ms-python.debugpy@2026.6.0`
- `ms-python.vscode-python-envs@1.36.0`
- `ms-toolsai.jupyter@2025.9.1`
- `ms-toolsai.jupyter-keymap@1.1.2`
- `ms-toolsai.jupyter-renderers@1.3.0`
- `ms-toolsai.vscode-ai@1.4.2`
- `ms-toolsai.vscode-ai-remote@1.4.9`
- `ms-toolsai.vscode-jupyter-cell-tags@0.1.9`
- `ms-toolsai.vscode-jupyter-slideshow@0.1.6`

---

### Description
When an agent or tool (`read_file`, `grep_search`, etc.) attempts to read a file located inside an open workspace folder that is a symlink pointing to an external directory (a very common layout in Azure ML compute instances, Docker/devcontainers, and Linux monorepos), Copilot Chat prompts the user with:
> `"Allow reading external files? links to , which is outside the current folder."`

Adding `` (or its parent directory) to `github.copilot.chat.additionalReadAccessPaths` in `settings.json` **does not prevent this prompt**. The permission confirmation check logic short-circuits as soon as it determines the symlink itself is inside the workspace, delegating exclusively to `K9o` which only verifies against `workspaceFolders`, completely bypassing `additionalReadAccessPaths`.

---

### Reproducible Steps

1. Create a minimal reproduction environment:
```bash
mkdir -p /tmp/external_data
echo "secret_or_normal_data = 42" > /tmp/external_data/config.py

mkdir -p /tmp/demo_workspace
ln -s /tmp/external_data /tmp/demo_workspace/linked_data
```

2. Open `/tmp/demo_workspace` in VS Code as the workspace folder.

3. In VS Code User or Workspace `settings.json`, add the target directory to `additionalReadAccessPaths`:
```json
{
"github.copilot.chat.additionalReadAccessPaths": [
"/tmp/external_data"
]
}
```

4. In Copilot Chat (Agent mode), ask:
> "Inspect /tmp/demo_workspace/linked_data/config.py" (or trigger a `read_file` tool call on that path).

---

### Expected vs. Actual Behavior

- **Expected Behavior:**
Copilot Chat recognizes that the target `/tmp/external_data/config.py` is covered by `github.copilot.chat.additionalReadAccessPaths` and reads the file without prompting the user for manual confirmation.

- **Actual Behavior:**
Copilot Chat displays a modal/confirmation dialog:
> `"Allow reading external files? /tmp/demo_workspace/linked_data/config.py links to /tmp/external_data/config.py, which is outside the current folder."`

---

### Code Analysis / Root Cause

Looking at the Copilot extension file confirmation pipeline (`extension.js`):

```javascript
// Function Y9o
let g = new wm(r?.workingDirectory, workspaceFolders);
if (g.getFolder(fileUri)) return K9o(fileUri, f => g.getFolder(f));
if (r?.readOnly && sbr(fileUri, configService)) return { needsConfirmation: false, realPath: void 0 };
```

Where:
- `sbr(uri, configService)` checks whether `uri` matches any entry in `github.copilot.chat.additionalReadAccessPaths`.
- `g.getFolder(fileUri)` checks whether `fileUri` sits inside an open workspace folder.

1. Because the symlink `fileUri` (`/tmp/demo_workspace/linked_data/config.py`) is located within `/tmp/demo_workspace`, `g.getFolder(fileUri)` returns **truthy**.
2. Execution immediately returns `K9o(fileUri, f => g.getFolder(f))`.
3. Execution **never reaches** line 3 (`if (r?.readOnly && sbr(fileUri, configService))`).
4. Inside `K9o`:
```javascript
async function K9o(t, e) {
if (t.scheme !== jt.file) return { needsConfirmation: false, realPath: void 0 };
let n = e(t);
if (!n || n.scheme !== jt.file) return { needsConfirmation: false, realPath: void 0 };
let r = Dw(await U$t(t, n)); // resolves realpath
return ur.isEqual(r, t)
? { needsConfirmation: false, realPath: void 0 }
: { needsConfirmation: !(e(r) !== void 0), realPath: r };
}
```
5. `e(r)` executes `f => g.getFolder(f)` on the resolved target `r` (`/tmp/external_data/config.py`).
6. Because `/tmp/external_data` is not an open workspace folder in `workspaceFolders`, `e(r)` is `undefined`.
7. `needsConfirmation` evaluates to `!(undefined !== void 0)` which is **`true`**.
8. Result: Confirmation dialog is displayed despite the path being explicitly allowed in `additionalReadAccessPaths`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.