@@ -11,7 +11,7 @@ import {
1111 commandTargetsRestricted ,
1212 MEGA_CHAIN_SEGMENT_THRESHOLD ,
1313} from "./classify.js" ;
14- import { autoShellRuleForCall } from "./auto-shell-policy.js" ;
14+ import { autoShellRuleForCall , safeWorktreeCommand } from "./auto-shell-policy.js" ;
1515import { commandReferencesSensitivePath } from "../plugins/secret-guard-plugin.js" ;
1616import { runShellAuthzBlockReason } from "../shell/run-shell-authz.js" ;
1717import { matchesPattern , escapeGlobLiteral } from "./matcher.js" ;
@@ -84,8 +84,28 @@ function hasExactFullCommandGrant(
8484// preGrantGuardReason (which only needs to know whether one tripped).
8585type SegmentGuard = { kind : "secret" | "restricted" } ;
8686
87- function segmentGuard ( segment : string , isRestricted : ( path : string , isWrite : boolean ) => boolean ) : SegmentGuard | undefined {
87+ // `cwd`/`rootsProvider`, when both supplied, let a contained or
88+ // permitted-sibling `git worktree add/remove` destination (see
89+ // safeWorktreeCommand) skip the generic restricted-path scan below — the
90+ // same exemption auto mode already applies (autoShellRuleForCall) — so a
91+ // standing `git worktree *` grant gets a chance to match instead of the
92+ // destination forcing an ask on every call regardless of any grant. Omitted
93+ // (as from call sites with no cwd on hand) simply skips the exemption and
94+ // falls back to today's behavior.
95+ function segmentGuard (
96+ segment : string ,
97+ isRestricted : ( path : string , isWrite : boolean ) => boolean ,
98+ cwd ?: string ,
99+ rootsProvider ?: RootsProvider ,
100+ ) : SegmentGuard | undefined {
88101 if ( commandReferencesSensitivePath ( segment ) !== undefined ) return { kind : "secret" } ;
102+ if (
103+ cwd !== undefined &&
104+ rootsProvider !== undefined &&
105+ safeWorktreeCommand ( segment , isRestricted , cwd , rootsProvider ) === true
106+ ) {
107+ return undefined ;
108+ }
89109 if ( commandTargetsRestricted ( segment , isRestricted ) ) return { kind : "restricted" } ;
90110 return undefined ;
91111}
@@ -119,6 +139,7 @@ function bindRestrictedToProcessCwd(
119139export function preGrantGuardReason (
120140 request : PermissionRequest ,
121141 isRestricted : ( path : string , isWrite : boolean ) => boolean ,
142+ rootsProvider ?: RootsProvider ,
122143) : string | undefined {
123144 if ( request . tool !== "run_shell" ) return undefined ;
124145 const fullCommand = request . subject ;
@@ -131,7 +152,7 @@ export function preGrantGuardReason(
131152 const restricted =
132153 request . cwd !== undefined ? bindRestrictedToProcessCwd ( isRestricted , request . cwd ) : isRestricted ;
133154 for ( const segment of segments ) {
134- const guard = segmentGuard ( segment , restricted ) ;
155+ const guard = segmentGuard ( segment , restricted , request . cwd , rootsProvider ) ;
135156 if ( guard !== undefined ) {
136157 return guard . kind === "secret"
137158 ? `${ segment } references a sensitive path`
@@ -154,12 +175,13 @@ export function isRequestCoveredByGrant(
154175 activeProviderModel : string | undefined ,
155176 isRestricted : ( path : string , isWrite : boolean ) => boolean ,
156177 workspace : GrantWorkspace ,
178+ rootsProvider ?: RootsProvider ,
157179) : boolean {
158180 if ( ! grantScopeMatches ( approval , request . tool , activeProviderModel , request . cwd , workspace ) ) return false ;
159181 if ( request . tool !== "run_shell" ) {
160182 return matchesPattern ( request . subject , approval . pattern ) ;
161183 }
162- if ( preGrantGuardReason ( request , isRestricted ) !== undefined ) return false ;
184+ if ( preGrantGuardReason ( request , isRestricted , rootsProvider ) !== undefined ) return false ;
163185 const segments = splitChainedCommand ( request . subject ) . filter ( ( s ) => ! isShellCommentOnly ( s ) ) ;
164186 if ( segments . length === 0 ) return false ;
165187 if ( segments . length > 1 ) {
@@ -334,7 +356,7 @@ export function createPermissionGate(options: PermissionGateOptions): Permission
334356 persist ?.( approval , grant ) ;
335357 }
336358 options . onGrant ?.( approval , ( request ) =>
337- isRequestCoveredByGrant ( request , approval , activeProviderModel , isRestricted , grantWorkspace ( ) ) ,
359+ isRequestCoveredByGrant ( request , approval , activeProviderModel , isRestricted , grantWorkspace ( ) , rootsProvider ) ,
338360 ) ;
339361 } ;
340362
@@ -473,7 +495,7 @@ export function createPermissionGate(options: PermissionGateOptions): Permission
473495 // replay for a guarded one just because the pattern also matches it.
474496 // segmentGuard is the same guard preGrantGuardReason applies before
475497 // isRequestCoveredByGrant lets a queued request skip the prompt.
476- const guard = segmentGuard ( segment , isRestrictedHere ) ;
498+ const guard = segmentGuard ( segment , isRestrictedHere , effectiveCwd , rootsProvider ) ;
477499 if ( guard !== undefined ) {
478500 if ( guard . kind === "secret" ) anySecret = true ;
479501 needsOperator = true ;
0 commit comments