Kiro IDE 1.x controls what the agent can run with a permissions.yaml file. The matching rules are not obvious or well documented, so I tested the behavior on Windows with PowerShell. This post covers how matching works, what surprised me, and a short starter list of commands that are safe to allow.
Versions: most tests ran on Kiro 1.2.37 (October 5-7, 2026). On October 26, 2026 I re-ran a spot check of 12 commands on Kiro 1.4.26 (VS Code 1.131.0, Windows 10.0.26100). Every result matched. I did not re-run everything, so treat the details as observed behavior, not a spec.
Security philosophy
If you approach AI agent permissions with the perspective of a malicious agent hell-bent on breaking through, you will have to review every prompt, leading to user fatigue. There are too many bypasses.
Instead, I guard against serious errors, like an AI agent inferring "fix merge conflict" implies "and push to production." (That happened yesterday.)
If you need higher security, either review every prompt, run agents in a sandbox, or look into control through MCPs.
Where rules live
- User level:
~/.kiro/settings/permissions.yaml(on Windows,%USERPROFILE%\.kiro\settings\permissions.yaml). - Workspace level:
<repo>/.kiro/settings/permissions.yaml. I did not test this one. - Organization level: admins can deploy a managed policy that takes precedence. See the Kiro enterprise permissions docs.
- Choosing "Always allow" in a prompt can write a rule to a separate file under
~/.kiro/workspace-roots/<id>/permissions.yaml. Review that file now and then, because nothing else shows you those rules.
Kiro reloads the file on the next tool call. No restart is needed.
I maintain a local, personal Git repository to manage my permissions and AGENTS.md file for multiple agents. It also includes documentation similar to this page. After an update, I launch a deploy script to copy it to the user profile path above.
File format
Each rule has a capability (shell, fs_read, fs_write, web_fetch, web_search), an effect (allow, ask, deny), and a list of patterns.
rules:
- capability: shell
effect: allow
match:
- git status *
- git diff *
- capability: shell
effect: deny
match:
- git push -f *
- git push *--force*
YAML quoting trap
A parse error makes Kiro deny every tool call, including the agent's own file edits, until you fix the file by hand. Inside double quotes, YAML treats a backslash as an escape, so "b:\sql" fails to parse. Worse, "C:\repo" loads silently because \r is a valid escape, and it becomes a rule that never matches. Use single quotes or no quotes for Windows paths.
How matching works
| Behavior | Example |
|---|---|
| Rules match from the start of the command, word by word. | git status * matches git status --short. |
A trailing * also matches the bare command. You do not need both X and X *. | nvidia-smi * matches plain nvidia-smi. |
The space before * is a word boundary. | Get-Item * does not match Get-ItemProperty. |
A rule without * is exact. | Write-Output does not match Write-Output 'hi'. |
A * in the middle spans multiple words. | snow.exe * --help matches snow.exe stage list --help. |
| The program name is literal. Each spelling is a different name. | git, git.exe, and a full path to git need separate rules. |
| A flag placed before the subcommand breaks the match. This applies to deny rules too. | git -C . status does not match git status *, and git -C . push -f escapes git push -f *. |
| Matching is case-sensitive, for the command name and for text inside arguments. | GIT status and get-date do not match rules written in the usual case. |
| Quoting style and flag order are literal text. | A pattern ending in " will not match single quotes or -q ... -c x order. |
A command that starts with ( does not match a rule starting with the cmdlet name. | (Get-Item x).Length prompts even with Get-Item * allowed. Use Get-Item x | Select-Object Length instead. |
Compound commands
Kiro splits a command on ;, &&, and | and checks each part on its own.
- Allow: every part must match an allow rule. If one part is unmatched, you get a prompt for the whole command.
- Deny: if any part matches a deny rule, the whole command is blocked before anything runs. This holds wherever the denied part sits: first, last, after a pipe, or after
cd. A deny also wins over an unmatched part, so you get a block and no prompt. - Subexpressions: code inside
$(...)is checked, and so are recognized cmdlets insideWhere-Object { }and& { }script blocks. A denied command inside is blocked, and anask-listed command inside prompts. - Navigation:
cd x; git statusneeds bothcd *and a git rule.
Results from my tests, where "allowed" means both parts have allow rules:
| Command | Result |
|---|---|
nvidia-smi -L; where.exe git (both allowed) | Ran, no prompt |
nvidia-smi -L; hostname (second part unmatched) | Prompt for the whole command |
hostname; git push --force (unmatched + denied) | Blocked, no prompt |
git status --short && git push -f origin main | Blocked |
git status --short $(git push -f origin main) | Blocked |
Precedence
- A command with no matching rule prompts you.
- Deny beats allow, and deny also beats Kiro's built-in allow list.
- Kiro has a built-in auto-allow list that is separate from your file. It covers some read-only commands for plain
git(status,log,show,blame,rev-parse). It did not covergit branch,git -C ..., or anything starting withgit.exe. When you test rules, use a command that is not on that list, such asgit.exe, or you cannot tell whether your rule or the built-in list allowed it.
Gotchas
The deny list is a speed bump, not a security control
- Case: PowerShell is case-insensitive but Kiro's matching is not.
FORMAT-VOLUMEwas not caught by deny rules forFormat-Volume *orformat-volume *. Add lowercase variants, knowing that mixed case still escapes. - Aliases:
iexwas not caught by a deny rule forInvoke-Expression *. I expect the same for other aliases but only tested this one. - Force pushes without
-f:git push origin +mainforce-pushed past a rule set that blocked-fand--force. Remote branch deletion (--delete,-d,origin :branch) needs its own rules. - SQL keywords: a deny pattern like
snow.exe sql * -q "*DROP*"missed lowercase, single quotes,--query, a different flag order, and thesnowspelling without.exe. Broader patterns such assnow sql *DROP*catch more, and these blocked the lowercase and no-.exeforms on 1.4.26. Mixed case (Drop),-f file.sql, and piped input can still escape. The real control is a least-privilege database role.
The practical protection is that unknown commands still prompt, plus limits from the operating system itself.
Read commands with write flags
npm audit *includesnpm audit fix. Denynpm audit fix *.git log *andgit diff *include--output=<file>, which writes a file. A deny rule ongit log *--output*blocked it, even thoughgit log *was allowed.- Redirection: an approved
Get-Date > file.txtran and wrote the file. Any broad allow rule for a cmdlet that produces output can overwrite files this way. I did not find a rule-based fix. - File readers such as
cat *andGet-Content *read any file, including.envfiles and keys, and they bypassfs_readlimits.
Be careful with * in allow rules
Because the middle * spans words, git -C * status * would also match git -C x push origin status. Keep write-capable commands out of broad allow patterns. In deny rules, a broad * is the safe direction. Over-blocking costs you a retry, not a mistake.
Deny messages are long
The automatic deny message lists every pattern in the matching rule block, not just the one that matched. For compound commands it ends with Triggered by: "<part>". If you want readable messages, split your deny rules into smaller blocks.
Starter allow list
These are read-only or low-risk, and common in agent workflows. Add the .exe spelling where your tools use it.
rules:
- capability: shell
effect: allow
match:
# Navigation, so "cd x; cmd" can pass when cmd is allowed
- cd *
- Set-Location *
# Git, read-only (list both spellings)
- git status *
- git.exe status *
- git diff *
- git.exe diff *
- git log *
- git.exe log *
- git show *
- git.exe show *
- git blame *
- git.exe blame *
- git rev-parse *
- git.exe rev-parse *
- git remote -v
- git.exe remote -v
- git branch --show-current
- git.exe branch --show-current
# PowerShell read-only cmdlets
- Get-Command *
- Get-Date *
- Get-Item *
- Get-ChildItem *
- Test-Path *
- Select-String *
- Select-Object *
- Where-Object *
# Lookups
- where.exe *
# Python checks that do not run your code
- python -m py_compile *
- python --version
# npm, read-only
- npm ls *
- npm audit *
A few choices behind that list:
- I left out
git -C ...on purpose. Runcd <repo>; git statusinstead, which works with the rules above. - I left out
Get-Contentandcat. They are popular, but they can read secrets. Add them if you accept that. - I left out
npm testandpython -m pytest. They run project code, so allow them only for repositories you trust. - Python is a program-name match like git. If the agent calls it by full path, you need a rule for that path (single-quoted in YAML).
Starter deny and ask list
Put these in separate blocks from your allow rules. Use ask for things you sometimes want, and deny for things you never want an agent to do.
- capability: shell
effect: ask
match:
- Remove-Item *
- remove-item *
- Clear-Content *
- clear-content *
- Stop-Process *
- stop-process *
- capability: shell
effect: deny
match:
# Destructive git history operations (both spellings)
- git push -f *
- git push * -f
- git push * -f *
- git push *--force*
- git push * +*
- git push *--delete*
- git push -d *
- git push * -d *
- git push * :*
- git -C * push *
- git filter-branch *
- git filter-repo *
- git reflog expire *
- git.exe push -f *
- git.exe push * -f
- git.exe push * -f *
- git.exe push *--force*
- git.exe push * +*
- git.exe push *--delete*
- git.exe push -d *
- git.exe push * -d *
- git.exe push * :*
- git.exe -C * push *
- git.exe filter-branch *
- git.exe filter-repo *
- git.exe reflog expire *
# Write flags on otherwise read-only commands
- git log *--output*
- git.exe log *--output*
- git diff *--output*
- git.exe diff *--output*
- npm audit fix *
# High-impact PowerShell (add lowercase copies; see case gotcha)
- Invoke-Expression *
- invoke-expression *
- Start-Process *
- start-process *
- Set-ExecutionPolicy *
- set-executionpolicy *
- Format-Volume *
- format-volume *
- Restart-Computer *
- restart-computer *
In my tests, the same ask rule for Remove-Item also prompted when the command sat inside a Where-Object { } or & { } block.
Checking your own rules
- Test with commands that are harmless if they run: a nonexistent drive,
-WhatIf, a nonexistent connection name, a repo with no remote. - Do not click "Always allow" or "Always deny" during tests. They can write rules into a permissions file behind your back.
- Add a control for each test. For example, test
a; bwith an unmatchedbfirst. - The agent only sees the tool result text. It can tell an automatic deny from a user rejection, but it cannot tell an auto-allowed run from one you approved by hand.
Steering
Combine permissions with steering. For example, it's not enough to deny git -C, I instruct agents to use an alternate form that works better with Kiro.
Summary
- Patterns match from the start of the command, word by word, and are case-sensitive.
- A trailing
*covers the bare command and any arguments. - Compound commands are split on
;,&&, and|. Allow needs every part to match. One denied part blocks everything. - List each program spelling separately (
git,git.exe). - Treat deny rules as a speed bump. Case, aliases, and unusual flags can get around them.
- Watch for read commands with write flags, and for redirection.
These notes come from hands-on testing, not from Kiro's source or a complete specification. Behavior may change between versions.
No comments:
Post a Comment