{"record":{"id":"c0b485c912db4c87","repo":"can1357/oh-my-pi","slug":"search-scope-entries-must-be-non-empty-paths-or-gl","errorCode":null,"errorMessage":"Search scope entries must be non-empty paths or globs","messagePattern":"Search scope entries must be non-empty paths or globs","errorType":"validation","errorClass":"ToolError","httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/tools/path-utils.ts","lineNumber":1498,"sourceCode":"\tmultiTargets?: ResolvedSearchTarget[];\n\texactFilePaths?: string[];\n\tmissingPaths: string[];\n\timmutableSourcePaths: Set<string>;\n}\n\n/**\n * Shared path-input pipeline for `search`, `ast_grep`, and `ast_edit`:\n *  1. normalize + reject empty paths,\n *  2. resolve internal URLs through {@link InternalUrlRouter} to backing files,\n *  3. partition existing vs missing when multiple paths are supplied,\n *  4. derive a single search base path / glob, or a multi-target list,\n *  5. stat the resolved base path so callers can branch on directory vs file scope.\n */\nexport async function resolveToolSearchScope(opts: ToolScopeOptions): Promise<ToolScopeResolution> {\n\tconst { rawPaths: inputs, cwd, internalUrlAction } = opts;\n\tconst normalizedRawPaths = inputs.map(normalizePathLikeInput);\n\tif (normalizedRawPaths.some(rawPath => rawPath.length === 0)) {\n\t\tthrow new ToolError(\"Search scope entries must be non-empty paths or globs\");\n\t}\n\tconst rawPaths = await expandDelimitedPathEntries(normalizedRawPaths, cwd);\n\tif (rawPaths.some(rawPath => rawPath.length === 0)) {\n\t\tthrow new ToolError(\"Search scope entries must be non-empty paths or globs\");\n\t}\n\t// Strict external-URL schemes. `file://` is intentionally absent: it has\n\t// local-path semantics (expandPath strips it downstream), so it flows through\n\t// the ordinary filesystem pipeline instead of the external-URL resolver.\n\tconst strictExternalUrlRe = /^(?:https?|ftp|ws|wss):\\/\\//i;\n\tconst internalRouter = InternalUrlRouter.instance();\n\tconst resolvedPathInputs: string[] = [];\n\tconst immutableSourcePaths = new Set<string>();\n\tfor (const rawPath of rawPaths) {\n\t\tlet externalUrl = strictExternalUrlRe.test(rawPath);\n\t\tif (!externalUrl && isReadableUrlPath(rawPath) && !hasGlobPathChars(rawPath)) {\n\t\t\t// Fuzzy spelling the read parser accepts (`www.host/…`, collapsed\n\t\t\t// `https:/host/…`). An existing local path wins over URL\n\t\t\t// interpretation so a directory literally named `www.foo` stays","sourceCodeStart":1480,"sourceCodeEnd":1516,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/tools/path-utils.ts#L1480-L1516","documentation":"resolveToolSearchScope validates every raw scope entry before use and throws a ToolError if any entry is an empty string after normalization. Search scope entries must name a real path or glob; empty strings indicate a malformed call.","triggerScenarios":"Calling resolveToolSearchScope (backing search/ast_grep/ast_edit) with `rawPaths` containing `\"\"` — e.g. an empty element from splitting a path list on a delimiter, or an unset variable interpolated into the paths array.","commonSituations":"Splitting a comma/space-separated path string that has a trailing or double delimiter (`src,,lib`); an environment variable or config field that is empty being pushed into the paths array; agent emitting `paths: [\"\"]`.","solutions":["Filter empty strings out of the paths array before calling: `paths.filter(p => p.trim().length > 0)`.","Fix the upstream split that produced the empty entry (guard against trailing/duplicate delimiters).","If no scope is intended, omit the entry or pass the cwd explicitly instead of an empty string."],"exampleFix":"// before\nsearch({ paths: input.split(\",\") }) // \"src,,lib\" -> [\"src\",\"\",\"lib\"]\n// after\nsearch({ paths: input.split(\",\").map(s => s.trim()).filter(Boolean) })","handlingStrategy":"validation","validationCode":"const paths = rawInput.split(\",\").map(s => s.trim()).filter(Boolean);\nif (paths.length === 0) throw new Error(\"At least one non-empty search path is required\");","typeGuard":"const hasEmptyEntry = (paths) => !Array.isArray(paths) || paths.some(p => typeof p !== \"string\" || p.trim().length === 0);","tryCatchPattern":"try { await resolveToolSearchScope({ rawPaths, cwd, internalUrlAction: \"search\" }); }\ncatch (e) { if (String(e.message).includes(\"non-empty\")) { rawPaths = rawPaths.filter(Boolean); /* retry */ } else throw e; }","preventionTips":["Always trim + filter path arrays before passing to search tools.","Guard delimiter-based splits against trailing/duplicate separators.","Validate user/config-supplied path lists at the boundary."],"tags":["validation","search","path","empty-input"],"backgroundTag":"empty-path-argument","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}