@@ -158,6 +158,106 @@ describe('explainCommand edge cases', () => {
158158 const result = explainCommand ( 'git status' , { cwd : '/tmp' } ) ;
159159 expect ( result . result ) . toBe ( 'allowed' ) ;
160160 } ) ;
161+
162+ test ( 'rm redirect to dev null is allowed when target is safe' , ( ) => {
163+ const result = explainCommand ( 'rm -rf /tmp/foo 2>/dev/null' ) ;
164+ expect ( result . result ) . toBe ( 'allowed' ) ;
165+ expect ( result . trace . steps ) . toContainEqual ( {
166+ type : 'parse' ,
167+ input : 'rm -rf /tmp/foo 2>/dev/null' ,
168+ segments : [ [ 'rm' , '-rf' , '/tmp/foo' ] ] ,
169+ } ) ;
170+ } ) ;
171+
172+ test ( 'redirect target command substitution remains blocked' , ( ) => {
173+ const result = explainCommand ( 'echo x >$(git reset --hard)' ) ;
174+ expect ( result . result ) . toBe ( 'blocked' ) ;
175+ expect ( result . reason ) . toContain ( 'git reset --hard' ) ;
176+ } ) ;
177+
178+ test ( 'nested rm redirect to dev null is allowed in command substitution' , ( ) => {
179+ const result = explainCommand ( 'echo $(rm -rf /tmp/foo 2>/dev/null)' ) ;
180+ expect ( result . result ) . toBe ( 'allowed' ) ;
181+ expect ( result . trace . steps ) . toContainEqual ( {
182+ type : 'parse' ,
183+ input : 'echo $(rm -rf /tmp/foo 2>/dev/null)' ,
184+ segments : [ [ 'echo' ] , [ 'rm' , '-rf' , '/tmp/foo' ] ] ,
185+ } ) ;
186+ } ) ;
187+
188+ test ( 'numeric rm target before redirect is preserved in explain trace' , ( ) => {
189+ const result = explainCommand ( 'rm -rf 7 > /dev/null' ) ;
190+ expect ( result . result ) . toBe ( 'allowed' ) ;
191+ expect ( result . trace . steps ) . toContainEqual ( {
192+ type : 'parse' ,
193+ input : 'rm -rf 7 > /dev/null' ,
194+ segments : [ [ 'rm' , '-rf' , '7' ] ] ,
195+ } ) ;
196+ } ) ;
197+
198+ test ( 'attached io-number redirect is stripped from explain trace' , ( ) => {
199+ const result = explainCommand ( 'rm -rf 123>/dev/null' ) ;
200+ expect ( result . result ) . toBe ( 'allowed' ) ;
201+ expect ( result . trace . steps ) . toContainEqual ( {
202+ type : 'parse' ,
203+ input : 'rm -rf 123>/dev/null' ,
204+ segments : [ [ 'rm' , '-rf' ] ] ,
205+ } ) ;
206+ } ) ;
207+
208+ test ( 'spaced numeric rm arg before redirect stays visible in explain trace' , ( ) => {
209+ const result = explainCommand ( 'rm -rf 123 >/dev/null' ) ;
210+ expect ( result . result ) . toBe ( 'allowed' ) ;
211+ expect ( result . trace . steps ) . toContainEqual ( {
212+ type : 'parse' ,
213+ input : 'rm -rf 123 >/dev/null' ,
214+ segments : [ [ 'rm' , '-rf' , '123' ] ] ,
215+ } ) ;
216+ } ) ;
217+
218+ test ( 'backticks inside arithmetic expansion remain blocked in explain trace' , ( ) => {
219+ const result = explainCommand ( 'echo $((`git reset --hard` + 1))' ) ;
220+ expect ( result . result ) . toBe ( 'blocked' ) ;
221+ expect ( result . reason ) . toContain ( 'git reset --hard' ) ;
222+ } ) ;
223+
224+ test ( 'process substitution remains blocked in explain trace' , ( ) => {
225+ const result = explainCommand ( 'echo <(git reset --hard)' ) ;
226+ expect ( result . result ) . toBe ( 'blocked' ) ;
227+ expect ( result . reason ) . toContain ( 'git reset --hard' ) ;
228+ expect ( result . trace . steps ) . toContainEqual ( {
229+ type : 'parse' ,
230+ input : 'echo <(git reset --hard)' ,
231+ segments : [ [ 'echo' ] , [ 'git' , 'reset' , '--hard' ] ] ,
232+ } ) ;
233+ } ) ;
234+
235+ test ( 'quoted literal backticks in redirect target do not hide blocked args in explain trace' , ( ) => {
236+ const result = explainCommand ( "git checkout >'file`name' -- foo" ) ;
237+ expect ( result . result ) . toBe ( 'blocked' ) ;
238+ expect ( result . reason ) . toContain ( 'git checkout --' ) ;
239+ expect ( result . trace . steps ) . toContainEqual ( {
240+ type : 'parse' ,
241+ input : "git checkout >'file`name' -- foo" ,
242+ segments : [ [ 'git' , 'checkout' , '--' , 'foo' ] ] ,
243+ } ) ;
244+ } ) ;
245+
246+ test ( 'single-quoted backticks in redirect targets stay literal in explain trace' , ( ) => {
247+ const result = explainCommand ( "echo >'a`git reset --hard`b'" ) ;
248+ expect ( result . result ) . toBe ( 'allowed' ) ;
249+ expect ( result . trace . steps ) . toContainEqual ( {
250+ type : 'parse' ,
251+ input : "echo >'a`git reset --hard`b'" ,
252+ segments : [ [ 'echo' ] ] ,
253+ } ) ;
254+ } ) ;
255+
256+ test ( 'attached backtick substitutions outside redirect targets stay blocked in explain trace' , ( ) => {
257+ const result = explainCommand ( 'echo foo`git reset --hard`bar' ) ;
258+ expect ( result . result ) . toBe ( 'blocked' ) ;
259+ expect ( result . reason ) . toContain ( 'git reset --hard' ) ;
260+ } ) ;
161261} ) ;
162262
163263describe ( 'explainCommand rm with home directory' , ( ) => {
0 commit comments