@@ -123,8 +123,8 @@ describe("ensureOpenClawCliOnPath", () => {
123123 expect ( process . env . PATH ) . toBe ( "/bin" ) ;
124124 } ) ;
125125
126- it ( "prepends mise shims when available " , ( ) => {
127- const { tmp, appBinDir , appCli } = setupAppCliRoot ( "case-mise" ) ;
126+ it ( "appends mise shims after system dirs " , ( ) => {
127+ const { tmp, appCli } = setupAppCliRoot ( "case-mise" ) ;
128128 const miseDataDir = path . join ( tmp , "mise" ) ;
129129 const shimsDir = path . join ( miseDataDir , "shims" ) ;
130130 setDir ( miseDataDir ) ;
@@ -140,10 +140,10 @@ describe("ensureOpenClawCliOnPath", () => {
140140 homeDir : tmp ,
141141 platform : "darwin" ,
142142 } ) ;
143- const appBinIndex = updated . indexOf ( appBinDir ) ;
143+ const usrBinIndex = updated . indexOf ( "/usr/bin" ) ;
144144 const shimsIndex = updated . indexOf ( shimsDir ) ;
145- expect ( appBinIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
146- expect ( shimsIndex ) . toBeGreaterThan ( appBinIndex ) ;
145+ expect ( usrBinIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
146+ expect ( shimsIndex ) . toBeGreaterThan ( usrBinIndex ) ;
147147 } ) ;
148148
149149 it . each ( [
@@ -222,7 +222,85 @@ describe("ensureOpenClawCliOnPath", () => {
222222 expect ( updated . indexOf ( xdgBinHome ) ) . toBeLessThan ( updated . indexOf ( localBin ) ) ;
223223 } ) ;
224224
225- it ( "prepends Linuxbrew dirs when present" , ( ) => {
225+ it ( "places ~/.local/bin AFTER /usr/bin to prevent PATH hijack" , ( ) => {
226+ const { tmp, appCli } = setupAppCliRoot ( "case-path-hijack" ) ;
227+ const localBin = path . join ( tmp , ".local" , "bin" ) ;
228+ setDir ( path . join ( tmp , ".local" ) ) ;
229+ setDir ( localBin ) ;
230+
231+ process . env . PATH = "/usr/bin:/bin" ;
232+ delete process . env . OPENCLAW_PATH_BOOTSTRAPPED ;
233+ delete process . env . XDG_BIN_HOME ;
234+
235+ const updated = bootstrapPath ( {
236+ execPath : appCli ,
237+ cwd : tmp ,
238+ homeDir : tmp ,
239+ platform : "linux" ,
240+ } ) ;
241+ const usrBinIndex = updated . indexOf ( "/usr/bin" ) ;
242+ const localBinIndex = updated . indexOf ( localBin ) ;
243+ expect ( usrBinIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
244+ expect ( localBinIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
245+ expect ( localBinIndex ) . toBeGreaterThan ( usrBinIndex ) ;
246+ } ) ;
247+
248+ it ( "places all user-writable home dirs after system dirs" , ( ) => {
249+ const { tmp, appCli } = setupAppCliRoot ( "case-user-writable-after-system" ) ;
250+ const localBin = path . join ( tmp , ".local" , "bin" ) ;
251+ const pnpmBin = path . join ( tmp , ".local" , "share" , "pnpm" ) ;
252+ const bunBin = path . join ( tmp , ".bun" , "bin" ) ;
253+ const yarnBin = path . join ( tmp , ".yarn" , "bin" ) ;
254+ setDir ( path . join ( tmp , ".local" ) ) ;
255+ setDir ( localBin ) ;
256+ setDir ( path . join ( tmp , ".local" , "share" ) ) ;
257+ setDir ( pnpmBin ) ;
258+ setDir ( path . join ( tmp , ".bun" ) ) ;
259+ setDir ( bunBin ) ;
260+ setDir ( path . join ( tmp , ".yarn" ) ) ;
261+ setDir ( yarnBin ) ;
262+
263+ process . env . PATH = "/usr/bin:/bin" ;
264+ delete process . env . OPENCLAW_PATH_BOOTSTRAPPED ;
265+ delete process . env . XDG_BIN_HOME ;
266+
267+ const updated = bootstrapPath ( {
268+ execPath : appCli ,
269+ cwd : tmp ,
270+ homeDir : tmp ,
271+ platform : "linux" ,
272+ } ) ;
273+ const usrBinIndex = updated . indexOf ( "/usr/bin" ) ;
274+ for ( const userDir of [ localBin , pnpmBin , bunBin , yarnBin ] ) {
275+ const idx = updated . indexOf ( userDir ) ;
276+ expect ( idx , `${ userDir } should come after /usr/bin` ) . toBeGreaterThan ( usrBinIndex ) ;
277+ }
278+ } ) ;
279+
280+ it ( "appends Homebrew dirs after immutable OS dirs" , ( ) => {
281+ const { tmp, appCli } = setupAppCliRoot ( "case-homebrew-after-system" ) ;
282+ setDir ( "/opt/homebrew/bin" ) ;
283+ setDir ( "/usr/local/bin" ) ;
284+
285+ process . env . PATH = "/usr/bin:/bin" ;
286+ delete process . env . OPENCLAW_PATH_BOOTSTRAPPED ;
287+ delete process . env . HOMEBREW_PREFIX ;
288+ delete process . env . HOMEBREW_BREW_FILE ;
289+ delete process . env . XDG_BIN_HOME ;
290+
291+ const updated = bootstrapPath ( {
292+ execPath : appCli ,
293+ cwd : tmp ,
294+ homeDir : tmp ,
295+ platform : "darwin" ,
296+ } ) ;
297+ const usrBinIndex = updated . indexOf ( "/usr/bin" ) ;
298+ expect ( usrBinIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
299+ expect ( updated . indexOf ( "/opt/homebrew/bin" ) ) . toBeGreaterThan ( usrBinIndex ) ;
300+ expect ( updated . indexOf ( "/usr/local/bin" ) ) . toBeGreaterThan ( usrBinIndex ) ;
301+ } ) ;
302+
303+ it ( "appends Linuxbrew dirs after system dirs" , ( ) => {
226304 const tmp = abs ( "/tmp/openclaw-path/case-linuxbrew" ) ;
227305 const execDir = path . join ( tmp , "exec" ) ;
228306 setDir ( tmp ) ;
@@ -247,7 +325,9 @@ describe("ensureOpenClawCliOnPath", () => {
247325 homeDir : tmp ,
248326 platform : "linux" ,
249327 } ) ;
250- expect ( parts [ 0 ] ) . toBe ( linuxbrewBin ) ;
251- expect ( parts [ 1 ] ) . toBe ( linuxbrewSbin ) ;
328+ const usrBinIndex = parts . indexOf ( "/usr/bin" ) ;
329+ expect ( usrBinIndex ) . toBeGreaterThanOrEqual ( 0 ) ;
330+ expect ( parts . indexOf ( linuxbrewBin ) ) . toBeGreaterThan ( usrBinIndex ) ;
331+ expect ( parts . indexOf ( linuxbrewSbin ) ) . toBeGreaterThan ( usrBinIndex ) ;
252332 } ) ;
253333} ) ;
0 commit comments