A policy says who may do what, and whether it is allowed or denied. This page walks through writing one: the fields, how Kei orders policies that overlap, which sources and destinations to choose, and what kei harness sync writes into a desktop harness. For the full decision model, see Policy priority and precedence. To grant an agent access through a group, follow Give a group access.
Kei does not seed policies. Administrators create policies for their workspace.
Anatomy of a policy#
| Field | What it holds | Example |
|---|---|---|
src |
Who the policy applies to | group:admins, user:<id>, agent:<id>, * |
dst |
What it matches | shell:git push --force, tool:registered_function |
effect |
permit or deny |
deny |
priority |
An integer; higher decides first | 100 |
enabled |
Whether the policy takes part in decisions | true |
How overlapping policies are ordered#
When several enabled policies match, Kei orders them and the first one decides:
- Priority, highest first.
- At equal priority, deny before allow.
- Policy ID, ascending.
When no policy matches, the result depends on where the request is decided. kei-proxy authorize fails closed: SDK (Agentware) integrations and connector operations get a deny. A desktop harness (Claude Code, Codex, OpenCode) has no rendered rule for the command, so it makes its own native decision, usually ask. Pi and Open WebUI use their own integrations.
Choose a source#
- People and agents are the normal sources. Use
user:<id>,group:<name>,agent:<id>, orautogroup:memberfor every organization member. These apply whichever harness the person or agent is using. harness:<kind>orharness:<id>is optional and narrower. Use it only for a rule that differs by harness, such as “Codex may never runrm”. It applies to everyone using that harness, not to a person. Aharness:source is valid only with a harness destination (shell:,skill:,path:,mcp:, or a harness tool such astool:claude_code.edit). Saving aharness:source with a connector, capability, resource, or other tool destination is rejected.agent:<id>with an exact tool or capability is checked at call time. The policy matches when its source matches either the user or the calling agent. That means a broad agent allow can grant around a user’s narrower rules. To cap it, add a higher-priority deny for the operation you want to exclude.
| Source | Destination | Effect | Priority |
|---|---|---|---|
agent:<id> |
shell:git push --force |
deny | 60 |
agent:<id> |
shell:git |
allow | 50 |
Choose a destination#
shell:<prefix>: a whole-token command prefix.shell:gitmatchesgit push, notgitx.skill:<name>: an exact skill name.path:<glob>: an absolute path glob, such aspath:/repo/**.tool:<name>: one exact tool-call name, with no wildcards.capability:<name>: one exact capability, such ascapability:pull_request.read.- All tools on a connector: in the console, pick a connection and check All tools on this connector. The policy covers every tool bound to that connection, including tools added later. A higher-priority
denyon one tool still carves it out.
Write the policy#
In the console: open Policies. Use Create agent command policy for shell commands, skills, and file paths, and Create Policy for tool calls and connectors. Choose Who and What, set Effect, and set Priority under Advanced.
With the CLI:
kei policies create --name "allow git" --src-pattern "*" \
--dst-pattern "shell:git" --effect permit --priority 10To bring an existing allow list across, kei policies import --from claude (or codex, opencode) prints the policies it would create; add --apply to create them.
After a change, run kei harness sync so desktop harnesses pick it up.
What renders into a desktop harness#
kei harness sync writes shell:, skill:, and path: policies into each harness’s native settings. A * source always renders. A user:, email:, or group: source renders only when it matches the bundle’s subject: the user who owns the runtime installation, their email, or one of their groups. A policy for another user or a group the subject is not in is skipped. Current policy bundles carry the subject. Bundles from older control planes do not, so none of those sources render there, and sync prints a line saying so.
A deny that cannot render still holds#
| Source | Destination | Effect | Priority |
|---|---|---|---|
group:admins |
shell:git push --force |
deny | 100 |
* |
shell:git |
allow | 10 |
When the bundle has no subject, or the subject is not in admins, the deny is skipped. Rendering the allow on its own would let git push --force run without asking, so sync withholds the allow too and prints a line for it:
$ kei harness sync --harness claude_code --dry-run
not rendered in Claude Code: allow git (a higher-precedence user:/group: deny overlaps it; the harness will ask)~/.claude/settings.json gets no git entry, so Claude Code asks before git push --force, and before any other git command. A skipped deny never becomes an allow.
When the subject is in admins, both policies render and Claude Code denies the push natively:
"allow": ["Bash(npm test)", "Bash(git:*)"],
"deny": ["Bash(git push --force:*)"]An allow that does not overlap the skipped deny still renders. For example, adding * shell:git status at priority 10 renders Bash(git status:*), so git status runs without asking while git push --force still asks.
Checklist#
- Start from people and agents. Add
harness:only for a per-harness difference. - Give each rule a distinct priority. Guardrail denies go highest.
- Cap broad agent allows with a higher-priority deny.
- Run
kei harness sync --dry-runand read everynot renderedline before you sync.