@@ -125,7 +125,7 @@ function parseChainedCommand(cmd) {
125125}
126126
127127// src/pre-tool-use.ts
128- var DENY_RULES = [
128+ var HARD_DENY_RULES = [
129129 { pattern : / ^ r m \s + - r f \s + \/ (?: \s | $ ) / i, reason : "Filesystem root deletion blocked" } ,
130130 { pattern : / ^ r m \s + - r f \s + \/ \* (?: \s | $ ) / i, reason : "Destructive wildcard deletion from root blocked" } ,
131131 { pattern : / ^ r m \s + - r f \s + ~ (?: \/ | $ ) / i, reason : "Home directory deletion blocked" } ,
@@ -151,6 +151,26 @@ var DENY_RULES = [
151151 reason : "find -exec/-execdir/-delete blocked: potential arbitrary command execution or recursive deletion"
152152 }
153153] ;
154+ var SOFT_DENY_RULES = [
155+ { pattern : / ^ g i t \s + p u s h \s + - - f o r c e (?: - w i t h - l e a s e ) ? \b / i, reason : "Force push needs user intent verification" } ,
156+ { pattern : / ^ g i t \s + p u s h \s + .* \s - (? ! - ) \S * f / i, reason : "Force push (short flag) needs user intent verification" } ,
157+ { pattern : / ^ g i t \s + p u s h \s + (?: .* \s ) ? (?: o r i g i n \s + ) ? ( m a i n | m a s t e r ) \s * $ / i, reason : "Push to default branch needs user intent verification" } ,
158+ { pattern : / ^ g i t \s + r e s e t \s + - - h a r d \b / i, reason : "Hard reset needs user intent verification" } ,
159+ { pattern : / ^ g i t \s + c l e a n \s + - [ a - z ] * f / i, reason : "Git clean needs user intent verification" } ,
160+ { pattern : / ^ g i t \s + b r a n c h \s + - [ a - z A - Z ] * D / i, reason : "Force branch delete needs user intent verification" } ,
161+ { pattern : / ^ n p m \s + p u b l i s h \b / i, reason : "Package publish needs user intent verification" } ,
162+ { pattern : / ^ ( t e r r a f o r m | p u l u m i ) \s + a p p l y \b / i, reason : "Infrastructure apply needs user intent verification" } ,
163+ { pattern : / ^ ( t e r r a f o r m | p u l u m i ) \s + d e s t r o y \b / i, reason : "Infrastructure destroy needs user intent verification" } ,
164+ { pattern : / ^ k u b e c t l \s + ( a p p l y | d e l e t e ) \b / i, reason : "Kubernetes mutation needs user intent verification" } ,
165+ { pattern : / \b ( \. c l a u d e \/ s e t t i n g s | C L A U D E \. m d ) \b / i, reason : "Agent self-modification needs user intent verification" } ,
166+ { pattern : / ^ g i t \s + c o m m i t \s + .* - - n o - v e r i f y \b / i, reason : "Skipping commit verification needs user intent verification" } ,
167+ { pattern : / \b c h m o d \s + 7 7 7 \b / i, reason : "Broad permission change needs user intent verification" } ,
168+ { pattern : / \b ( n c | n c a t | s o c a t ) \s + - l / i, reason : "Exposing local service needs user intent verification" } ,
169+ { pattern : / \b p y t h o n 3 ? \s + - m \s + h t t p \. s e r v e r / i, reason : "Exposing HTTP server needs user intent verification" } ,
170+ { pattern : / \b ( c r o n t a b | s y s t e m c t l \s + e n a b l e | s s h - k e y g e n | s s h - c o p y - i d ) \b / i, reason : "Unauthorized persistence needs user intent verification" } ,
171+ { pattern : / \b ( g c l o u d \s + .* a d d - i a m | a w s \s + i a m | a z \s + r o l e \s + a s s i g n m e n t ) \b / i, reason : "Permission grant needs user intent verification" } ,
172+ { pattern : / \b s y s t e m c t l \s + s t o p \s + .* l o g / i, reason : "Logging tampering needs user intent verification" }
173+ ] ;
154174var ALLOW_RULES = [
155175 {
156176 pattern : / ^ ( n p m | y a r n | p n p m | b u n ) \s + ( t e s t | r u n | i n s t a l l | c i | a d d | r e m o v e | l s | i n f o | o u t d a t e d | a u d i t | w h y ) \b / i,
@@ -177,6 +197,63 @@ var ALLOW_RULES = [
177197 reason : "Safe docker read operation"
178198 }
179199] ;
200+ var WRITE_EDIT_SOFT_DENY_PATTERNS = [
201+ { pattern : / (?: ^ | [ / \\ ] ) \. e n v (?: \. | $ ) / i, reason : "Writing to .env file needs user intent verification" } ,
202+ { pattern : / (?: ^ | [ / \\ ] ) \. c l a u d e [ / \\ ] s e t t i n g s / i, reason : "Writing to .claude/settings needs user intent verification" } ,
203+ { pattern : / (?: ^ | [ / \\ ] ) C L A U D E \. m d $ / i, reason : "Writing to CLAUDE.md needs user intent verification" } ,
204+ { pattern : / (?: ^ | [ / \\ ] ) \. g i t h u b [ / \\ ] w o r k f l o w s [ / \\ ] / i, reason : "Writing to CI/CD config needs user intent verification" } ,
205+ { pattern : / (?: ^ | [ / \\ ] ) \. g i t l a b - c i \. y m l $ / i, reason : "Writing to CI/CD config needs user intent verification" } ,
206+ { pattern : / (?: ^ | [ / \\ ] ) J e n k i n s f i l e $ / i, reason : "Writing to CI/CD config needs user intent verification" } ,
207+ { pattern : / (?: ^ | [ / \\ ] ) \. c i r c l e c i [ / \\ ] / i, reason : "Writing to CI/CD config needs user intent verification" }
208+ ] ;
209+ function classifyWriteEdit ( filePath ) {
210+ if ( ! filePath ) {
211+ return null ;
212+ }
213+ for ( const rule of WRITE_EDIT_SOFT_DENY_PATTERNS ) {
214+ if ( rule . pattern . test ( filePath ) ) {
215+ return { decision : "soft_deny" , reason : rule . reason } ;
216+ }
217+ }
218+ if ( ! filePath . startsWith ( "/" ) || filePath . includes ( "/node_modules/" ) ) {
219+ return { decision : "allow" , reason : "Safe project file write" } ;
220+ }
221+ return null ;
222+ }
223+ var WEBFETCH_SOFT_DENY_PATTERNS = [
224+ { pattern : / \b ( p a s t e b i n \. c o m | p a s t e \. e e | h a s t e b i n \. c o m | d p a s t e \. o r g | g h o s t b i n \. c o m | r e n t r y \. c o ) \b / i, reason : "Paste service needs user intent verification" } ,
225+ { pattern : / \b ( t r a n s f e r \. s h | f i l e \. i o | 0 x 0 \. s t | t m p f i l e s \. o r g ) \b / i, reason : "File sharing service needs user intent verification" } ,
226+ { pattern : / \. ( s h | b a s h | p s 1 | b a t | c m d ) ( \? | $ ) / i, reason : "Script download needs user intent verification" } ,
227+ { pattern : / \b r a w \. g i t h u b u s e r c o n t e n t \. c o m \/ .* \. ( s h | p y | r b | j s ) $ / i, reason : "Raw script download needs user intent verification" }
228+ ] ;
229+ function classifyWebFetch ( url ) {
230+ if ( ! url ) {
231+ return null ;
232+ }
233+ for ( const rule of WEBFETCH_SOFT_DENY_PATTERNS ) {
234+ if ( rule . pattern . test ( url ) ) {
235+ return { decision : "soft_deny" , reason : rule . reason } ;
236+ }
237+ }
238+ if ( / ^ h t t p s ? : \/ \/ ( l o c a l h o s t | 1 2 7 \. 0 \. 0 \. 1 | 0 \. 0 \. 0 \. 0 ) ( : \d + ) ? / i. test ( url ) ) {
239+ return { decision : "allow" , reason : "Safe localhost request" } ;
240+ }
241+ return null ;
242+ }
243+ var SAFE_TOOLS = new Set ( [
244+ "Read" ,
245+ "Glob" ,
246+ "Grep" ,
247+ "LS" ,
248+ "Search" ,
249+ "TaskCreate" ,
250+ "TaskUpdate" ,
251+ "TaskList" ,
252+ "TaskGet" ,
253+ "TodoRead" ,
254+ "TodoWrite" ,
255+ "NotebookRead"
256+ ] ) ;
180257function isGitPushNonForce ( cmd ) {
181258 return / ^ g i t \s + p u s h \b / i. test ( cmd ) && ! / - - f o r c e (?: - w i t h - l e a s e ) ? \b | \s - (? ! - ) \S * f / i. test ( cmd ) ;
182259}
@@ -197,9 +274,14 @@ function evaluateSingleCommand(cmd) {
197274 if ( ! cmd . trim ( ) ) {
198275 return null ;
199276 }
200- for ( const rule of DENY_RULES ) {
277+ for ( const rule of HARD_DENY_RULES ) {
201278 if ( rule . pattern . test ( cmd ) ) {
202- return { decision : "deny" , reason : rule . reason } ;
279+ return { decision : "hard_deny" , reason : rule . reason } ;
280+ }
281+ }
282+ for ( const rule of SOFT_DENY_RULES ) {
283+ if ( rule . pattern . test ( cmd ) ) {
284+ return { decision : "soft_deny" , reason : rule . reason } ;
203285 }
204286 }
205287 for ( const rule of ALLOW_RULES ) {
@@ -212,15 +294,27 @@ function evaluateSingleCommand(cmd) {
212294 }
213295 return null ;
214296}
215- function evaluate ( input ) {
216- if ( input . tool_name !== "Bash" ) {
217- return null ;
297+ function decisionToOutput ( decision , reason , label ) {
298+ switch ( decision ) {
299+ case "hard_deny" :
300+ process . stderr . write ( `gatekeeper: deny "${ label } " — ${ reason }
301+ ` ) ;
302+ return makeDecision ( "deny" , reason ) ;
303+ case "soft_deny" :
304+ process . stderr . write ( `gatekeeper: soft_deny "${ label } " — ${ reason }
305+ ` ) ;
306+ return null ;
307+ case "allow" :
308+ process . stderr . write ( `gatekeeper: allow "${ label } " — ${ reason }
309+ ` ) ;
310+ return makeDecision ( "allow" , reason ) ;
218311 }
219- const cmd = ( input . tool_input ?. command ?? "" ) . trim ( ) ;
312+ }
313+ function evaluateBash ( cmd ) {
220314 if ( ! cmd ) {
221315 return null ;
222316 }
223- for ( const rule of DENY_RULES ) {
317+ for ( const rule of HARD_DENY_RULES ) {
224318 if ( rule . pattern . test ( cmd ) ) {
225319 process . stderr . write ( `gatekeeper: deny "${ cmd } " — ${ rule . reason }
226320` ) ;
@@ -236,9 +330,7 @@ function evaluate(input) {
236330 if ( parsed . kind === "single" ) {
237331 const result = evaluateSingleCommand ( cmd ) ;
238332 if ( result ) {
239- process . stderr . write ( `gatekeeper: ${ result . decision } "${ cmd } " — ${ result . reason }
240- ` ) ;
241- return makeDecision ( result . decision , result . reason ) ;
333+ return decisionToOutput ( result . decision , result . reason , cmd ) ;
242334 }
243335 process . stderr . write ( `gatekeeper: passthrough "${ cmd } " — no matching rule
244336` ) ;
@@ -248,13 +340,13 @@ function evaluate(input) {
248340 let firstAllowedResult = null ;
249341 for ( const part of parsed . parts ) {
250342 const result = evaluateSingleCommand ( part ) ;
251- if ( result ?. decision === "deny " ) {
343+ if ( result ?. decision === "hard_deny " ) {
252344 process . stderr . write ( `gatekeeper: deny "${ cmd } " — part "${ part } ": ${ result . reason }
253345` ) ;
254346 return makeDecision ( "deny" , result . reason ) ;
255347 }
256- if ( result === null ) {
257- process . stderr . write ( `gatekeeper: passthrough "${ cmd } " — unknown part "${ part } "
348+ if ( result ?. decision === "soft_deny" || result === null ) {
349+ process . stderr . write ( `gatekeeper: passthrough "${ cmd } " — ${ result ? "soft_deny" : " unknown" } part "${ part } "
258350` ) ;
259351 return null ;
260352 }
@@ -273,6 +365,42 @@ function evaluate(input) {
273365` ) ;
274366 return makeDecision ( "allow" , compositeReason ) ;
275367}
368+ function evaluate ( input ) {
369+ const toolName = input . tool_name ;
370+ const toolInput = input . tool_input ;
371+ if ( SAFE_TOOLS . has ( toolName ) ) {
372+ process . stderr . write ( `gatekeeper: allow "${ toolName } " — safe tool
373+ ` ) ;
374+ return makeDecision ( "allow" , `Safe tool: ${ toolName } ` ) ;
375+ }
376+ if ( toolName === "Bash" ) {
377+ const cmd = ( toolInput ?. command ?? "" ) . trim ( ) ;
378+ return evaluateBash ( cmd ) ;
379+ }
380+ if ( toolName === "Write" || toolName === "Edit" ) {
381+ const filePath = toolInput ?. file_path ?? "" ;
382+ const result = classifyWriteEdit ( filePath ) ;
383+ if ( result ) {
384+ return decisionToOutput ( result . decision , result . reason , `${ toolName } :${ filePath } ` ) ;
385+ }
386+ process . stderr . write ( `gatekeeper: passthrough "${ toolName } :${ filePath } " — no matching rule
387+ ` ) ;
388+ return null ;
389+ }
390+ if ( toolName === "WebFetch" ) {
391+ const url = toolInput ?. url ?? "" ;
392+ const result = classifyWebFetch ( url ) ;
393+ if ( result ) {
394+ return decisionToOutput ( result . decision , result . reason , `WebFetch:${ url } ` ) ;
395+ }
396+ process . stderr . write ( `gatekeeper: passthrough "WebFetch:${ url } " — no matching rule
397+ ` ) ;
398+ return null ;
399+ }
400+ process . stderr . write ( `gatekeeper: passthrough "${ toolName } " — unknown tool
401+ ` ) ;
402+ return null ;
403+ }
276404function readStdin ( ) {
277405 return new Promise ( ( resolve , reject ) => {
278406 let data = "" ;
@@ -320,6 +448,9 @@ export {
320448 isGitPushNonForce ,
321449 evaluateSingleCommand ,
322450 evaluate ,
323- DENY_RULES ,
451+ classifyWriteEdit ,
452+ classifyWebFetch ,
453+ SOFT_DENY_RULES ,
454+ HARD_DENY_RULES ,
324455 ALLOW_RULES
325456} ;
0 commit comments