@@ -20,7 +20,10 @@ import {
2020 safeWorktreeCommand ,
2121 isWorktreeForceFlag ,
2222} from "./auto-shell-policy.js" ;
23- import { commandReferencesSensitivePath } from "../plugins/secret-guard-plugin.js" ;
23+ import {
24+ commandReferencesSensitivePath ,
25+ createExtraDeniedPathMatcher ,
26+ } from "../plugins/secret-guard-plugin.js" ;
2427import {
2528 normalizePathArguments ,
2629 pathEscapeBlockReason ,
@@ -133,8 +136,9 @@ function segmentGuard(
133136 isRestricted : ( path : string , isWrite : boolean ) => boolean ,
134137 cwd ?: string ,
135138 rootsProvider ?: RootsProvider ,
139+ isExtraDenied : ( value : string ) => boolean = ( ) => false ,
136140) : SegmentGuard | undefined {
137- if ( commandReferencesSensitivePath ( segment , cwd ) !== undefined )
141+ if ( commandReferencesSensitivePath ( segment , cwd , isExtraDenied ) !== undefined )
138142 return { kind : "secret" } ;
139143 if (
140144 cwd !== undefined &&
@@ -223,6 +227,7 @@ export function preGrantGuardReason(
223227 request : PermissionRequest ,
224228 isRestricted : ( path : string , isWrite : boolean ) => boolean ,
225229 rootsProvider ?: RootsProvider ,
230+ isExtraDenied : ( value : string ) => boolean = ( ) => false ,
226231) : string | undefined {
227232 if ( request . tool !== "run_shell" ) return undefined ;
228233 const fullCommand = request . subject ;
@@ -239,7 +244,13 @@ export function preGrantGuardReason(
239244 ? bindRestrictedToProcessCwd ( isRestricted , request . cwd )
240245 : isRestricted ;
241246 for ( const segment of segments ) {
242- const guard = segmentGuard ( segment , restricted , request . cwd , rootsProvider ) ;
247+ const guard = segmentGuard (
248+ segment ,
249+ restricted ,
250+ request . cwd ,
251+ rootsProvider ,
252+ isExtraDenied ,
253+ ) ;
243254 if ( guard !== undefined ) {
244255 return guard . kind === "secret"
245256 ? `${ segment } references a sensitive path`
@@ -268,6 +279,7 @@ export function isRequestCoveredByGrant(
268279 isRestricted : ( path : string , isWrite : boolean ) => boolean ,
269280 workspace : GrantWorkspace ,
270281 rootsProvider ?: RootsProvider ,
282+ isExtraDenied : ( value : string ) => boolean = ( ) => false ,
271283) : boolean {
272284 return isRequestCoveredByApprovals (
273285 request ,
@@ -276,6 +288,7 @@ export function isRequestCoveredByGrant(
276288 isRestricted ,
277289 workspace ,
278290 rootsProvider ,
291+ isExtraDenied ,
279292 ) ;
280293}
281294
@@ -290,6 +303,7 @@ function isRequestCoveredByApprovals(
290303 isRestricted : ( path : string , isWrite : boolean ) => boolean ,
291304 workspace : GrantWorkspace ,
292305 rootsProvider ?: RootsProvider ,
306+ isExtraDenied : ( value : string ) => boolean = ( ) => false ,
293307) : boolean {
294308 const scoped = approvals . filter ( ( a ) =>
295309 grantScopeMatches (
@@ -304,7 +318,10 @@ function isRequestCoveredByApprovals(
304318 if ( request . tool !== "run_shell" ) {
305319 return scoped . some ( ( a ) => matchesPattern ( request . subject , a . pattern ) ) ;
306320 }
307- if ( preGrantGuardReason ( request , isRestricted , rootsProvider ) !== undefined )
321+ if (
322+ preGrantGuardReason ( request , isRestricted , rootsProvider , isExtraDenied ) !==
323+ undefined
324+ )
308325 return false ;
309326 const segments = splitChainedCommand ( request . subject ) . filter (
310327 ( s ) => ! isShellCommentOnly ( s ) ,
@@ -313,7 +330,12 @@ function isRequestCoveredByApprovals(
313330 const cwd = request . cwd ?? workspace . resolvedCwd ;
314331 return segments . every ( ( segment ) => {
315332 if ( scoped . some ( ( a ) => matchesPattern ( segment , a . pattern ) ) ) return true ;
316- return isAutoAllowedShellSegment ( segment , cwd , rootsProvider ) ;
333+ return isAutoAllowedShellSegment (
334+ segment ,
335+ cwd ,
336+ rootsProvider ,
337+ isExtraDenied ,
338+ ) ;
317339 } ) ;
318340}
319341
@@ -382,6 +404,13 @@ export interface PermissionGateOptions {
382404 // Writes and deletes remain path-escape denies; plugin trust is not write
383405 // consent.
384406 trustedPluginRoots ?: RootsProvider ;
407+ // Extras-denied config paths the shell legs treat as sensitive (CL-9386):
408+ // the active settings source, including a --config override. Mirrors the
409+ // secret-guard plugin's extraDeniedPaths so a custom config path asks in
410+ // shell commands exactly like the default settings file. May also be set
411+ // after construction via setSensitiveExtraDeniedPaths when the paths are
412+ // learned later (the toolset builder forwards them).
413+ sensitiveExtraDeniedPaths ?: readonly string [ ] ;
385414 // Tiers learned from connected MCP servers (tools/list annotations). Tests may
386415 // inject a shared registry; production gates create one when omitted.
387416 mcpTiers ?: McpToolPermissionRegistry ;
@@ -488,6 +517,11 @@ export interface PermissionGate {
488517 // posix plugin stack reads this so authorize-time and execution-time
489518 // containment share one list.
490519 getTrustedPluginRoots : ( ) => readonly string [ ] ;
520+ // Replace the extras-denied config paths the shell legs treat as sensitive
521+ // (CL-9386). The toolset builder calls this to forward the active settings
522+ // source after gate construction, so the gate and the secret-guard plugin
523+ // share one list. Optional so test doubles of this interface keep compiling.
524+ setSensitiveExtraDeniedPaths ?: ( paths : readonly string [ ] ) => void ;
491525}
492526
493527// True when splitChainedCommand can be trusted to yield only real segments for
@@ -570,6 +604,14 @@ export function createPermissionGate(
570604 // Session grants live only in this array; persisted grants are seeded in via
571605 // options.approvals and re-routed to a store by the persist callback.
572606 const sessionGrants : Approval [ ] = [ ] ;
607+ // CL-9386: the extras-denied config paths (the active settings source) the
608+ // shell legs consult, mirrored from the secret-guard plugin's own matcher so
609+ // both agree on what "denied" means. Mutable via
610+ // setSensitiveExtraDeniedPaths because the toolset builder learns the paths
611+ // after the gate is constructed.
612+ let isExtraDenied = createExtraDeniedPathMatcher (
613+ options . sensitiveExtraDeniedPaths ?? [ ] ,
614+ ) ;
573615
574616 // Record an operator-granted approval in the live list and route it to the
575617 // scope-appropriate home: session grants stay in memory, everything else is
@@ -638,6 +680,7 @@ export function createPermissionGate(
638680 isRestricted ,
639681 grantWorkspace ( ) ,
640682 rootsProvider ,
683+ isExtraDenied ,
641684 ) ,
642685 ) ;
643686 }
@@ -798,14 +841,15 @@ export function createPermissionGate(
798841 // segment mentions a secret path.
799842 const shellReferencesSecret =
800843 shellCmd !== undefined &&
801- commandReferencesSensitivePath ( shellCmd , effectiveCwd ) !== undefined ;
844+ commandReferencesSensitivePath ( shellCmd , effectiveCwd , isExtraDenied ) !==
845+ undefined ;
802846 if ( ! restricted && classifyTool ( call . name , mcpTiers ) === "allow" ) {
803847 return { kind : "allow" } ;
804848 }
805849 if (
806850 ! restricted &&
807851 ! shellReferencesSecret &&
808- isAutoAllowedShellCall ( call , effectiveCwd , rootsProvider )
852+ isAutoAllowedShellCall ( call , effectiveCwd , rootsProvider , isExtraDenied )
809853 ) {
810854 return { kind : "allow" } ;
811855 }
@@ -821,6 +865,7 @@ export function createPermissionGate(
821865 isRestrictedHere ,
822866 effectiveCwd ,
823867 rootsProvider ,
868+ isExtraDenied ,
824869 ) ;
825870 if ( shellRule ?. effect === "deny" ) {
826871 recordAutoDecision ( call . name , shellRule . name , "auto-deny" ) ;
@@ -876,6 +921,7 @@ export function createPermissionGate(
876921 isRestrictedHere ,
877922 effectiveCwd ,
878923 rootsProvider ,
924+ isExtraDenied ,
879925 ) ;
880926 if ( guard !== undefined ) {
881927 if ( guard . kind === "secret" ) anySecret = true ;
@@ -912,7 +958,14 @@ export function createPermissionGate(
912958 }
913959 // Safe pipeline tails (`| sort`) and pure no-ops (`|| true`) skip.
914960 // Containment is judged against the process cwd, not the session cwd.
915- if ( isAutoAllowedShellSegment ( segment , effectiveCwd , rootsProvider ) ) {
961+ if (
962+ isAutoAllowedShellSegment (
963+ segment ,
964+ effectiveCwd ,
965+ rootsProvider ,
966+ isExtraDenied ,
967+ )
968+ ) {
916969 continue ;
917970 }
918971 needsOperator = true ;
@@ -1141,8 +1194,11 @@ export function createPermissionGate(
11411194 ) => {
11421195 const anySecret =
11431196 request . tool === "run_shell" &&
1144- commandReferencesSensitivePath ( request . subject , request . cwd ) !==
1145- undefined ;
1197+ commandReferencesSensitivePath (
1198+ request . subject ,
1199+ request . cwd ,
1200+ isExtraDenied ,
1201+ ) !== undefined ;
11461202 const decision = {
11471203 kind : "ask" as const ,
11481204 request,
@@ -1256,5 +1312,10 @@ export function createPermissionGate(
12561312 registerMcpClient,
12571313 unregisterMcpServer,
12581314 getTrustedPluginRoots : ( ) => trustedPluginRoots ( ) ,
1315+ setSensitiveExtraDeniedPaths : ( paths : readonly string [ ] ) => {
1316+ isExtraDenied = createExtraDeniedPathMatcher ( paths ) ;
1317+ // The denied set changed — cached denies re-evaluate.
1318+ denialMemory . clear ( ) ;
1319+ } ,
12591320 } ;
12601321}
0 commit comments