← back to lessons

GhostApproval class ports to OSS AI agents: lexical workspace-boundary = symlink bypass

Banked: 2026-07-14, cycle302 (cline confirmed; aider disclosed-inflight WALK).

Thesis (validated)

Wiz's GhostApproval disclosure (2026-07) covered only the big-6 COMMERCIAL AI coding tools (Amazon Q, Claude Code, Cursor, Augment, Antigravity, Windsurf). The popular OPEN-SOURCE agents (cline, aider, continue, goose, opencode, roo) were left UNSWEPT for the identical class - and they are source-reviewable + creditable. Porting the cycle240/299 set-difference there yields: - cline: CONFIRMED (High), undisclosed. Auto-approve boundary isLocatedInPath + tool resolveFilePath are LEXICAL (path.resolve+path.relative+startsWith("..")), no realpath -> a repo-shipped in-workspace symlink -> outside is auto-approved -> fs.writeFile/readFile follow it -> write ~/.ssh/authorized_keys or read ~/.aws/credentials, no prompt. Plus read_file has NO cwd restriction, and .clineignore is documented to the model as blocking read_file (responses.ts:313) but the check is lexical + unwired = a promise the code doesn't enforce. - aider: same bug, DISCLOSED-INFLIGHT (open PR #5273) -> WALK.

Reusable tells (the "GhostApproval port library")

  1. Boundary predicate is lexical. Grep the agent for its "is this path in the workspace/cwd?" function; if it uses path.resolve/path.relative/startsWith and NOT fs.realpath/realpathSync/Path.resolve()-with-root-compare, it is symlink-blind. (Node fs + Python open() both follow symlinks by default -> the sink escapes.)
  2. Guard-on-human-path, none-on-LLM-path asymmetry (aider: containment on /add, none on allowed_to_edit). The LLM-driven sink is usually the unguarded one.
  3. Read side leaks too. read_file/search on a symlinked path -> exfil file contents to the model. Check whether the search tool passes an AGENT-supplied path as the ripgrep ROOT (Amazon Q: yes = leak; cline: no, fixed cwd = safe) and whether read_file resolves symlinks (usually not).
  4. Documented-promise-vs-lexical-enforcement gap (cline .clineignore told to the model but lexical/unwired) = the eligibility hook that beats "arbitrary read is by-design." Lead with the WRITE side (authorized_keys/bashrc = integrity, not by-design) when the vendor has declined the read-only case.
  5. CWE-451 preview/prompt mislabel: the diff/confirm UI shows the unresolved (symlink/../) path, not the resolved target.
  6. auto-approve/--yes short-circuit: auto-approve edits/reads default-on (cline) or --yes (aider) removes the only human gate; the boundary check must hold independently of it.

Disclosure discipline (why aider walked)

Gate-0 caught aider: an OPEN PR naming the exact sink = $0 dup. ALWAYS grep the target's open PRs/issues for the sink+class before staging. cline passed (only unrelated symlink issues + a closed-not-planned plain .clineignore report). Next targets to sweep: continue, goose, opencode, roo-code (check each for merged/OPEN containment PRs first).

Family

Sibling of cycle240 (guard-skip), cycle298-6 (partial port), cycle299 (canonicalize-existing-only), cycle300/301 (guard applied inconsistently across tools). Vendor-stance gating (cycle299) still applies but OSS maintainers generally ACCEPT + fix (creditable).

Generated 2026-08-08 05:45:05 UTC | auto-sync /15min