PermDrift: AI Agent Permission Monitor
PermDrift watches the files that decide what your AI coding agents may do without asking -
run shell commands, edit files, fetch URLs, call MCP tools - and tells you when those
permissions get wider. It compares every permission file with its committed version (git HEAD),
flags each grant that widens agent permissions on the exact line, and warns you live when an
agent or another process widens them on disk.
Works with Claude Code, OpenAI Codex CLI, VS Code / GitHub Copilot agent mode, MCP clients and
Gemini CLI.
Why
Agent permissions drift. Every "Yes, and don't ask again" adds an allow rule. A tired
Bash(npm test *) becomes Bash(npm *), then Bash. A mode moves from default to
acceptEdits to bypassPermissions. Sometimes the agent edits its own permission file. None of
this shows up anywhere you normally look, and a one-line change in a JSON file is easy to wave
through in code review.
PermDrift makes that drift visible:
- In the editor: diagnostics on the exact line, with "before -> after" and a safer alternative.
- In the status bar:
Perms: 2 widened · 1 unrestricted, highlighted when something widened or safety is off.
- Live: when a permission file is widened on disk by something other than your own VS Code save, a
warning names what was added (
+ Bash(git push *)) with a one-click diff against HEAD.
- In review: a Markdown permission report to paste into the PR.
Supported agents and files
| Agent |
Files |
What is read |
| Claude Code |
.claude/settings.json, .claude/settings.local.json |
permissions.allow / ask / deny, permissions.defaultMode, permissions.additionalDirectories, permissions.disableBypassPermissionsMode, enableAllProjectMcpServers, enabledMcpjsonServers |
| Claude Code |
.claude/agents/*.md |
frontmatter permissionMode, tools |
| Claude Code |
.claude/skills/**/SKILL.md |
frontmatter allowed-tools |
| OpenAI Codex CLI |
.codex/config.toml, codex.toml |
approval_policy, sandbox_mode, [sandbox_workspace_write] network_access (also inside [profiles.*]) |
| VS Code / Copilot agent mode |
.vscode/settings.json |
chat.tools.global.autoApprove, chat.permissions.default, chat.tools.terminal.autoApprove, chat.tools.terminal.enableAutoApprove, chat.tools.edits.autoApprove, chat.tools.urls.autoApprove, claudeCode.initialPermissionMode |
| MCP clients |
.mcp.json, .vscode/mcp.json, .cursor/mcp.json, .roo/mcp.json |
per server alwaysAllow / autoApprove lists, trust |
| Gemini CLI |
.gemini/settings.json |
tools.allowed, general.defaultApprovalMode, security.disableYoloMode, mcpServers.*.trust |
Files are found anywhere in the workspace (monorepos included). JSON files may contain comments
and trailing commas.
Rules
| ID |
Rule |
Default |
Bad |
Better |
| PD001 |
Widened vs HEAD - any grant present now but not at HEAD that increases capability |
Warning |
HEAD "Bash(npm test *)", now "Bash(npm *)" |
Keep the committed rule, or add only the exact command you need |
| PD002 |
Unrestricted shell - the agent may run any command |
Warning |
"allow": ["Bash"], "Bash(*)", run_shell_command, terminal auto-approve "/.*/": true |
"Bash(npm test *)", "Bash(git status)" |
| PD003 |
Dangerous command grant - an allow rule that auto-approves a destructive or outbound command |
Warning |
"Bash(git push *)", "Bash(git *)" (covers push), "Bash(sudo *)", bare WebFetch plus shell access |
Put them under ask, or deny them |
| PD004 |
Safety off - prompts are disabled wholesale |
Error |
"defaultMode": "bypassPermissions", approval_policy = "never" + sandbox_mode = "danger-full-access", "chat.tools.global.autoApprove": true, MCP "alwaysAllow": ["*"], MCP "trust": true |
acceptEdits / default with narrow allow rules; workspace-write + on-request |
| PD005 |
Local file shadowing - .claude/settings.local.json (usually gitignored, so never reviewed) grants more than the committed .claude/settings.json |
Information |
local-only "Bash(docker *)" |
Move shared grants into settings.json so they are reviewed |
PD003 tests each shell grant against a curated list of sample commands: rm -rf, git push,
git reset --hard, curl/wget piped to a shell, sudo, chmod 777,
docker run --privileged, kubectl delete, terraform apply/destroy, npm publish,
ssh/scp. A broad rule like Bash(git *) is flagged because it covers git push. Add your
own samples with permDrift.extraDangerousCommands.
What counts as "widened" (PD001)
- a new allow rule that is not already covered by a committed allow rule (
Bash(npm *) replacing
Bash(npm test *) is shown as Bash(npm test *) -> Bash(npm *); adding the narrower
Bash(npm test *) next to Bash(npm *) is not flagged)
- a removed
deny or ask rule (unless a deny rule still covers it)
- a mode or policy moved up the permissiveness order:
| Setting |
Order, least -> most permissive |
Unset means |
Claude Code defaultMode, agent permissionMode, claudeCode.initialPermissionMode |
dontAsk < plan < default (manual) < acceptEdits < auto < bypassPermissions |
default |
Codex approval_policy |
untrusted < on-request = granular < on-failure < never |
on-request |
Codex sandbox_mode |
read-only < workspace-write < danger-full-access |
read-only |
VS Code chat.permissions.default |
default < autoApprove < autopilot |
default |
Gemini general.defaultApprovalMode |
plan < default < auto_edit < yolo |
default |
(dontAsk sounds permissive but only runs pre-approved tools and denies everything else, so it
is the most locked-down Claude Code mode.)
- a boolean switched on (
enableAllProjectMcpServers, chat.tools.global.autoApprove,
network_access, MCP trust), or a guard switched off (disableBypassPermissionsMode,
disableYoloMode, chat.tools.terminal.enableAutoApprove: false)
- a new entry in an auto-approve or allow list (
additionalDirectories, enabledMcpjsonServers,
MCP alwaysAllow/autoApprove, chat.tools.*.autoApprove, Gemini tools.allowed)
- a subagent whose
tools restriction was removed (it now inherits every tool)
Narrowing changes are never flagged.
How the HEAD comparison works
For each permission file PermDrift reads the committed version with
git show HEAD:./<file> (from the file's own folder), extracts the grants from both versions and
compares them. A file that is not in HEAD yet is reported once as a summary ("New file not in
HEAD: all 4 grant(s) are new") instead of line by line. .claude/settings.local.json is normally
gitignored, so it is checked against the committed settings.json instead (PD005).
The comparison refreshes when you edit, save, run PermDrift: Scan Now, or return to the
window (to pick up commits made in a terminal).
Live alerts. A watcher follows every supported file. When a file changes on disk and VS Code
did not save that file within the last 1.5 seconds (permDrift.saveWindowMs), the change is
treated as external - an agent, a script, a git checkout. If the new content widens permissions
compared with the previous content, you get a non-modal warning listing what was added, with
Show diff (HEAD vs working tree) and Dismiss. Your own editor saves never trigger it.
Commands
| Command |
What it does |
| PermDrift: Scan Now |
Re-read every permission file and refresh HEAD content |
| PermDrift: Show Widened Since HEAD |
Quick pick of every PD001 finding; pick one to jump to it |
| PermDrift: Copy Permission Report (Markdown) |
Findings grouped by agent and file, ready for a PR comment |
| PermDrift: Show HEAD Diff for Current File |
Side-by-side diff of the committed and working version |
| PermDrift: Show Agent Permissions |
Focus the Agent Permissions view in the Explorer (grouped by agent -> file -> finding; click to jump to the line) |
Quick Fixes (lightbulb, applied only when you pick them): remove a single widened entry from
a JSON array, or revert a mode / flag to its HEAD value. Nothing is ever changed automatically.
Settings
| Setting |
Default |
Description |
permDrift.enabled |
true |
Check agent permission files |
permDrift.compareWithHead |
true |
PD001 comparison with git HEAD (trusted workspaces only) |
permDrift.liveAlerts |
true |
Warn when a permission file is widened from outside the editor |
permDrift.saveWindowMs |
1500 |
Window that attributes a file change to your own VS Code save |
permDrift.severity |
{} |
Per rule: error, warning, information, hint or off, e.g. { "PD005": "off" } |
permDrift.extraDangerousCommands |
[] |
Extra sample commands for PD003, e.g. ["helm uninstall prod"] |
permDrift.maxFiles |
200 |
Maximum permission files scanned |
Privacy and security
- Everything runs locally. No network calls, no telemetry, no account.
- Only permission keys are read. MCP
env and headers blocks - where tokens usually live -
are never parsed into findings, so their values (and names) never appear in messages, the tree
or the report. Token-like strings inside rule text are masked as ***.
- Workspace Trust: in an untrusted workspace the static rules (PD002-PD005) and live alerts
still work because they only read files; the HEAD comparison (PD001) needs
git, which honors
repo-local configuration, so it turns on only after you trust the workspace.
- Every git call uses an argument list (no shell), ignores global and system git config, sets
GIT_OPTIONAL_LOCKS=0, and passes -c core.fsmonitor= --no-pager --no-ext-diff --no-textconv,
so a hostile repo cannot run an fsmonitor hook, pager, diff driver or textconv filter.
Limitations
- PermDrift reads project files only. User-level (
~/.claude/settings.json, ~/.codex/config.toml,
VS Code user settings) and managed / enterprise policy files are not compared.
- Rule matching follows the documented wildcard syntax (
*, trailing * and :*, mcp__server
prefixes) but is not the agents' exact matcher; unusual patterns may be misjudged as covering or
not covering each other.
- PD003 is a curated sample list, not a shell parser; wrappers and compound commands are not
expanded.
- The Codex reader understands the keys above, basic strings, booleans and one-level inline
tables - not the full TOML spec.
- External-change detection is a timing heuristic: a VS Code save and an agent write within the
same 1.5 seconds can be confused.
- Key names were checked against the official Claude Code, Codex, VS Code and Gemini CLI docs in
2026-09; tools rename settings, so unknown keys are simply ignored.
- agnix is a broad linter / language server for agent config files (CLAUDE.md, AGENTS.md,
skills, hooks, MCP). It validates configs statically - including warnings for
bypassPermissions and unrestricted Bash in skills, and whether Codex approval_policy /
sandbox_mode values are valid. It does not compare permissions against git history, rate
Codex approval/sandbox combinations or VS Code / MCP auto-approve settings for risk, or alert
you live when a permission file is widened. The two work fine side by side.
- The client-side Claude Code settings linter web tool at hidekazu-konishi.com checks a
settings file you paste into a web page. It covers Claude Code only and has no view of your git
history or live changes.
PermDrift's focus is narrower: the direction of change - what your agents can do now that
they could not do at the last commit - across several agents, inside the editor, as it happens.
License
MIT - the full license text is included with the extension.
| |