classifyDir nav scope escapes the wt (ls relDir lexical prefix)shared/classify.js classifyDir(be, …, scopePfx) opens scopeAbs = join(wtRoot,
scopePfx.slice(0,-1)) and readdirs it (:462, :502, :506), where
scopePfx arrives from a nav URI via views/ls/ls.js relDir() (:105, :146)
— a LEXICAL prefix computation that .. defeats. So ls //name/../../etc (or
ls:../../etc) yields scopePfx = "../../etc/", then readdir(join(wtRoot,
scopePfx)) escapes the worktree. The exact defeated-by-.. lexical-prefix
pattern BE-011 removed from wtdir, surviving in the ls scope path; found in
the BE-011 follow-up wt-open audit. Method Issues.
classify.js:462 scopeAbs = scopePfx ? join(wtRoot, scopePfx.slice(0,-1)) : wtRoot, then readdir(scopeAbs) (:502), lstat(join(scopeAbs, nm)) (:506).ls.js:146 scopePfx = relDir(repo.wt, absScope) — relDir (:105) is a lexical prefix strip, no .. collapse; absScope itself may already carry .. from the nav URI.path.js now has resolveInTree/wtJoin (throw NAVESCAPE) and discover.resolve() (nav-URI → confined abs).ls scope that resolves outside the wt is REFUSED (NAVESCAPE), never readdir'd.scopePfx via resolveInTree("", scopePfx) in classifyDir, OR have ls.js/list.js build absScope through discover.resolve(base, uri) instead of join(topWt, uri).ls //name/../../etc / ls:../../x → refusal).shared/util/path.js. Same tree (no worktree).ls.js/list.js compose the wt-relative scope via pathlib.wtJoin(topWt, uri) (resolveInTree-confine + join, THROWS NAVESCAPE on climb), replacing the lexical join(topWt, uri). Chose wtJoin over resolve(): the loop's authorityRepo already refuses/strips any //authority (the //name/.. forms ARE BE-011's NAVESCAPE), so inside ls the scope is a plain wt-relative path that must confine to the OWNING topWt — wtJoin does exactly that, whereas resolve("",…) re-roots an ABSOLUTE lsr recursion child against SRC_ROOT (wrong). The absolute branch (a trusted lsr child — real in-tree dir names) is left as-is.classifyDir runs scopePfx through pathlib.resolveInTree("", scopePfx) before the readdir/lstat, so ANY caller handing a ..-laden scope is refused (NAVESCAPE), never readdir'd outside the wt.ls with a .. scope escapes today (RED) — test/ls/scope-escape/run.sh.wtJoin/resolveInTree; suite green.test/ls/scope-escape/run.sh: ls ../outside / ls ls:../outside inside a root wt escaped and listed a SIBLING outside/secret.txt (exit 0); now REFUSED with NAVESCAPE, no leak.wtJoin confine in views/ls/ls.js + views/list/list.js; defensive resolveInTree floor in shared/classify.js classifyDir.test/ls/*, test/list/e2e, test/status/*, test/spot/submount all pass (the two submount cases SKIP without git-over-ssh; both PASS when run under $HOME).