@@ -14,7 +14,7 @@ import { autoShellRuleForCall } from "./auto-shell-policy.js";
1414import { commandReferencesSensitivePath } from "../plugins/secret-guard-plugin.js" ;
1515import { runShellAuthzBlockReason } from "../shell/run-shell-authz.js" ;
1616import { matchesPattern , escapeGlobLiteral } from "./matcher.js" ;
17- import { evaluateApprovals } from "./authz-grants.js" ;
17+ import { evaluateApprovals , cwdMatchesGrant , type GrantWorkspace } from "./authz-grants.js" ;
1818import { splitChainedCommand , tokenize , isShellCommentOnly , stripCommentLines } from "./command.js" ;
1919import { createPathRestriction } from "./path-restriction.js" ;
2020import { createWorktreeRootsProvider , type RootsProvider } from "./worktree-roots.js" ;
@@ -51,6 +51,7 @@ function hasExactFullCommandGrant(
5151 approvals : readonly Approval [ ] ,
5252 activeProviderModel : string | undefined ,
5353 requestCwd : string | undefined ,
54+ workspace : GrantWorkspace ,
5455) : boolean {
5556 // Comment-insensitive: a model-authored "# why" line prepended to an
5657 // otherwise-identical command must still replay against a grant minted
@@ -62,7 +63,7 @@ function hasExactFullCommandGrant(
6263 a . tool === tool &&
6364 a . pattern === normalized &&
6465 ( a . providerModel === undefined || a . providerModel === activeProviderModel ) &&
65- ( a . cwd === undefined || a . cwd === requestCwd ) ,
66+ cwdMatchesGrant ( a . cwd , requestCwd , workspace ) ,
6667 ) ;
6768}
6869
@@ -142,9 +143,10 @@ export function isRequestCoveredByGrant(
142143 approval : Approval ,
143144 activeProviderModel : string | undefined ,
144145 isRestricted : ( path : string , isWrite : boolean ) => boolean ,
146+ workspace : GrantWorkspace ,
145147) : boolean {
146148 if ( request . tool !== approval . tool ) return false ;
147- if ( approval . cwd !== undefined && approval . cwd !== request . cwd ) return false ;
149+ if ( ! cwdMatchesGrant ( approval . cwd , request . cwd , workspace ) ) return false ;
148150 if (
149151 approval . providerModel !== undefined &&
150152 approval . providerModel !== activeProviderModel
@@ -268,11 +270,15 @@ export function createPermissionGate(options: PermissionGateOptions): Permission
268270 const { requestApproval, persist, interactive, skipPermissions, providerName, model, cwd } = options ;
269271 const mcpTiers = options . mcpTiers ?? createMcpToolPermissionRegistry ( ) ;
270272 const resolvedCwd = cwd ?? process . cwd ( ) ;
271- const pathRestriction = createPathRestriction (
272- resolvedCwd ,
273- options . rootsProvider ?? createWorktreeRootsProvider ( resolvedCwd ) ,
274- ) ;
273+ const rootsProvider = options . rootsProvider ?? createWorktreeRootsProvider ( resolvedCwd ) ;
274+ const pathRestriction = createPathRestriction ( resolvedCwd , rootsProvider ) ;
275275 const isRestricted = pathRestriction . isRestricted ;
276+ // This gate's project boundary for grant matching (see cwdMatchesGrant):
277+ // this session's root plus its currently-known registered worktrees. Built
278+ // fresh per read from the same rootsProvider the gate already uses for
279+ // path containment, so "same project" for a grant and "inside the
280+ // workspace" for a path share one authority.
281+ const grantWorkspace = ( ) : GrantWorkspace => ( { resolvedCwd, roots : rootsProvider ( ) } ) ;
276282 let auto = options . auto ;
277283 // Own a private copy so evaluating a grant never mutates the caller's array.
278284 const approvals : Approval [ ] = [ ...options . approvals ] ;
@@ -309,7 +315,7 @@ export function createPermissionGate(options: PermissionGateOptions): Permission
309315 persist ?.( approval , grant ) ;
310316 }
311317 options . onGrant ?.( approval , ( request ) =>
312- isRequestCoveredByGrant ( request , approval , activeProviderModel , isRestricted ) ,
318+ isRequestCoveredByGrant ( request , approval , activeProviderModel , isRestricted , grantWorkspace ( ) ) ,
313319 ) ;
314320 } ;
315321
@@ -405,7 +411,14 @@ export function createPermissionGate(options: PermissionGateOptions): Permission
405411 ! fullReferencesSecret &&
406412 ! commandTargetsRestricted ( fullCommand , isRestrictedHere ) &&
407413 segments . length > 1 &&
408- hasExactFullCommandGrant ( request . tool , fullCommand , approvals , activeProviderModel , effectiveCwd )
414+ hasExactFullCommandGrant (
415+ request . tool ,
416+ fullCommand ,
417+ approvals ,
418+ activeProviderModel ,
419+ effectiveCwd ,
420+ grantWorkspace ( ) ,
421+ )
409422 ) {
410423 continue ;
411424 }
@@ -432,6 +445,7 @@ export function createPermissionGate(options: PermissionGateOptions): Permission
432445 approvals,
433446 activeProviderModel,
434447 requestCwd : effectiveCwd ,
448+ workspace : grantWorkspace ( ) ,
435449 } )
436450 ) {
437451 continue ;
@@ -502,6 +516,7 @@ export function createPermissionGate(options: PermissionGateOptions): Permission
502516 approvals,
503517 activeProviderModel,
504518 requestCwd : effectiveCwd ,
519+ workspace : grantWorkspace ( ) ,
505520 } ) ;
506521 if ( alreadyApproved ) {
507522 continue ;
0 commit comments