From 489ea52ab04d52e4748b6e52addd2b0c59089c08 Mon Sep 17 00:00:00 2001 From: burnlife001 Date: Sat, 13 Jun 2026 03:19:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(claude-local):=20address=20Greptile=20revie?= =?UTF-8?q?w=20=E2=80=94=20shared=20cache,=20tighter=20regex,=20managed=20?= =?UTF-8?q?boundary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from PR review: 1. Share contentCache across recursive calls instead of allocating a fresh Map per invocation. This fixes the bug where a file referenced both from the root AGENTS.md and from a nested sub-file would silently drop the nested occurrence because the inner call's empty cache had no entry. 2. Tighten regex from /@\S+\.md/ to /@\S+\/\S*\.md/ — require at least one "/" in the matched path so that bare @user.md mentions and email-like docs@company.md are never treated as file references. All genuine file refs carry a directory separator. 3. Only widen the security boundary by 2 levels when the bundle mode is explicitly "managed". External (user-configured) bundles keep their exact configured root as the boundary. Co-Authored-By: Claude Opus 4.7 --- .../claude-local/src/server/execute.ts | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/packages/adapters/claude-local/src/server/execute.ts b/packages/adapters/claude-local/src/server/execute.ts index 789ae01296..c960a25746 100644 --- a/packages/adapters/claude-local/src/server/execute.ts +++ b/packages/adapters/claude-local/src/server/execute.ts @@ -141,16 +141,19 @@ async function resolveAtReferences( content: string, baseDir: string, instructionsRoot: string, + contentCache: Map = new Map(), visited: Set = new Set(), depth: number = 0, ): Promise { const MAX_DEPTH = 5; if (depth > MAX_DEPTH) return content; - // Allow optional horizontal whitespace between @ and the path so that - // @ ./path.md @`./path.md` @(./path.md) @"./path.md" - // all match (not just the no-space forms). - const AT_REF_RE = /@[ \t]*(\S+\.md)/gi; + // Match @ followed by an optional path-like reference to a .md file. + // Require at least one "/" in the path so that bare words like @user.md + // or email-like strings (docs@company.md) are not treated as file refs. + // All genuine file refs carry a directory separator: @./file.md, + // @../../shared.md, @path/to/file.md. + const AT_REF_RE = /@[ \t]*(\S+\/\S*\.md)/gi; // First pass: find all @ references and resolve their absolute paths. // We collect replacement targets without mutating the string yet so that @@ -217,15 +220,12 @@ async function resolveAtReferences( // positions remain valid after later (higher-index) substitutions. replacements.sort((a, b) => b.start - a.start); - // Cache already-read file contents so that multiple @ references to the - // same file (e.g. the 4 syntax variants all pointing to Test.md) are all - // inlined rather than having the later ones skipped by the visited guard. - const contentCache = new Map(); - for (const { full, resolvedPath, start, end } of replacements) { if (visited.has(resolvedPath)) { // Still inline the file content even if already visited — multiple // @ references to the same file in the same document must all resolve. + // contentCache is shared across recursive calls so nested refs that + // point to a file already expanded at a higher depth are also served. const cached = contentCache.get(resolvedPath); if (cached !== undefined) { content = content.slice(0, start) + cached + content.slice(end); @@ -241,6 +241,7 @@ async function resolveAtReferences( refContent, path.dirname(resolvedPath), instructionsRoot, + contentCache, visited, depth + 1, ); @@ -571,14 +572,21 @@ export async function execute(ctx: AdapterExecutionContext): Promise/instructions/) and - // widened by 2 levels to encompass shared files at the …/agents/ level. + // to Claude Code. + // + // The security boundary is the instructions root directory. For managed + // bundles the root points at …/agents//instructions/ and shared + // files live at the …/agents/ level, so we widen the boundary by 2 + // directory levels to allow @../../shared_all.md to resolve. External + // (user-configured) bundles keep their exact root as the boundary — the + // operator chose that directory deliberately. const configuredRoot = asString(config.instructionsRootPath, "").trim(); + const bundleMode = asString(config.instructionsBundleMode, "").trim(); const instructionsRoot = configuredRoot || path.dirname(instructionsFilePath); - const instructionsBoundary = configuredRoot - ? path.resolve(instructionsRoot, "..", "..") - : instructionsRoot; + const instructionsBoundary = + configuredRoot && bundleMode === "managed" + ? path.resolve(instructionsRoot, "..", "..") + : instructionsRoot; instructionsContent = await resolveAtReferences( instructionsContent, path.dirname(instructionsFilePath),