Persistent Approvals Across Sessions
**Note: This work is currently on pause, see **[**this update**](https://gitlab.com/groups/gitlab-org/-/work_items/21877#note_3547608932)**.**
## Summary
The first iterations of tool approvals built session-scoped approvals. decisions a user makes during a workflow that reset when the session ends. This iteration persists those decisions to disk so users don't start from scratch every session.
Pre-approvals are file-based and operate at the user level via `~/.gitlab/duo`. File entries are read at Duo session start and converted into session approvals using the existing `tool_call_pattern_approval` infrastructure. The file supports a three-tier trust model: `allow` (pre-approved), `ask` (always prompt), and `deny` (auto-rejected).
---
## Motivation
Users currently lose all approval decisions when a session ends. Recurring workflows, a developer who always approves `git` commands, or always trusts a set of read-only tools, require re-approval on every new session. This creates unnecessary friction in agentic workflows without any security benefit for tools the user has already made a deliberate trust decision about.
---
## Proposed Change
Introduce a user-level YAML permissions file at `~/.gitlab/duo`:
```yaml
# ~/.gitlab/duo
version: 1
tool_approvals:
allow:
- tool: run_command
patterns:
- "npm *" # trust npm broadly
- tool: read_file # no patterns = all uses pre-approved
- tool: grep
ask:
- tool: run_command
patterns:
- "npm publish *" # always prompt, overrides the allow above
- "npm run deploy"
deny:
- tool: run_command
patterns:
- "rm -rf *"
- "sudo *"
- tool: delete_file # no patterns = all uses denied
```
### How it works
Precedence order (most restrictive wins): governance → `deny` → `ask` → `allow` → default (prompt).
| Entry type | v1 enforcement | Future |
|------------|----------------|--------|
| `allow` (with pattern) | Converted to pattern session approval via `UpdateToolCallApprovals` mutation at session start | — |
| `allow` (no pattern) | Converted to wildcard session approval at session start | — |
| `ask` | Client-side: LSP always fires the normal approval prompt, even if a broader `allow` would have matched | Client-side only, no backend work needed |
| `deny` | Client-side: LSP auto-rejects matching tool calls without prompting | Wired to backend once GraphQL mutation supports deny |
`ask` exists to carve out exceptions from broad `allow` rules. For example, `allow: npm *` with `ask: npm publish *` trusts npm broadly but always prompts for publish — without enumerating every other npm sub-command explicitly.
File is read on each Duo session start via the existing `/direct_access` path. Gated behind the `tool_call_pattern_approval` capability. Governance rules from the monolith (instance → group → project) override user file entries.
> **Note:** Project-level file (`.gitlab/duo`) is explicitly out of scope for this iteration. Governance at the namespace/group/project level already covers this use case. The architecture remains open to adding it in a future iteration.
---
:white_check_mark: - Done
:construction: - In Progress
:white_circle: - Not Started
### :sparkles: Core Foundational Elements
<table>
<tr>
<th></th>
<th>Description</th>
<th>Sub Epic / Issues</th>
<th>Status</th>
<th>Technical Lead</th>
</tr>
<tr>
<td>Determine persistent tool approval storage and security model</td>
<td>
* Spike to decide storage location, file format, and security model for persistent approvals.
</td>
<td>
https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/work_items/2114+
</td>
<td>
:white_check_mark:
</td>
<td>
@aspringfield / @dbernardi
</td>
</tr>
<tr>
<td>Finalize YAML file format for user tool permission settings</td>
<td>
* Define the canonical schema for `~/.gitlab/duo` including the three-tier allow/ask/deny structure, pattern format, and version field.
</td>
<td>
https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/work_items/2361+
</td>
<td>
:white_check_mark:
</td>
<td>
@dbernardi
</td>
</tr>
<tr>
<td>User file pre-approval POC for CLI</td>
<td>
* Proof of concept: read `~/.gitlab/duo` at session start, enforce ask/deny entries client-side, and convert allow entries to session approvals via the existing `UpdateToolCallApprovals` mutation.
</td>
<td>
https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/work_items/1647+
</td>
<td>
:white_circle:
</td>
<td>
@dbernardi
</td>
</tr>
<tr>
<td>
Read from user tool permissions file (\`\~/.gitlab/duo\`)
</td>
<td>
* Full LSP implementation: parse the user file, enforce deny and ask entries client-side, and wire allow entries to the session approval store on session init.
</td>
<td>
https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/work_items/2362+
</td>
<td>
:white_circle:
</td>
<td>
@dbernardi
</td>
</tr>
</table>
### :sparkles: Followup
<table>
<tr>
<th></th>
<th>Description</th>
<th>Sub Epic / Issues</th>
<th>Status</th>
<th>Technical Lead</th>
</tr>
<tr>
<td>
Add deny support to `updateDuoWorkflowToolCallApprovals` GraphQL mutation
</td>
<td>
* Extend the mutation to accept deny entries so they can be enforced at the backend level, not just client-side.
* No schema change to `~/.gitlab/duo` required when this lands.
</td>
<td>
https://gitlab.com/gitlab-org/gitlab/-/work_items/603232+
</td>
<td>
:white_circle:
</td>
<td>
@dbernardi
</td>
</tr>
</table>
## Dependencies
- Blocked by [&21850](https://gitlab.com/groups/gitlab-org/-/work_items/21850) — wildcard/pattern approvals must be stable before the file format can be finalized and the `tool_call_pattern_approval` capability gate is meaningful
- GraphQL endpoint for exposing merged tool permissions to the client (SSCS:Compliance — `@jeanvdw` / `@nrosandich`) — not a hard dependency for v1
## References
- Parent epic: https://gitlab.com/groups/gitlab-org/-/work_items/20519
- Spike async update: https://gitlab.com/gitlab-org/editor-extensions/gitlab-lsp/-/work_items/2114#note_3284384611
- Wildcard/pattern approvals sub-epic: https://gitlab.com/groups/gitlab-org/-/work_items/21850
- Layer 3 project-scoped policy overrides (control plane): https://gitlab.com/groups/gitlab-org/-/work_items/21390
epic