can1357/oh-my-pi · error · ToolError
Invalid regex: ${message.replace(/^Invalid regular expressio
Error message
Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")} What it means
The JS fallback matcher (jsMatchedLineIndexes) compiles the user pattern with new RegExp; an invalid pattern throws a SyntaxError which is converted to a clean ToolError 'Invalid regex: <detail>' with the engine's boilerplate prefix stripped.
Source
Thrown at packages/coding-agent/src/tools/grep.ts:438
* large for native grep (>`NATIVE_GREP_MAX_FILE_BYTES`, which native grep silently
* skips). Mirrors the native probe's output (sorted, deduped indexes) so
* `buildVirtualMatches` rebuilds context/ranges identically; only the regex dialect
* differs for these oversized inputs (the pre-RE2-parity behavior).
*/
function jsMatchedLineIndexes(
content: string,
lines: readonly string[],
pattern: string,
ignoreCase: boolean,
multiline: boolean,
): number[] {
const flags = `${ignoreCase ? "i" : ""}${multiline ? "gm" : ""}`;
let regex: RegExp;
try {
regex = new RegExp(pattern, flags);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw new ToolError(`Invalid regex: ${message.replace(/^Invalid regular expression:\s*/i, "")}`);
}
if (!multiline) {
const out: number[] = [];
for (let i = 0; i < lines.length; i++) {
regex.lastIndex = 0;
if (regex.test(lines[i] ?? "")) out.push(i);
}
return out;
}
const { starts } = indexSearchLines(content);
const seen = new Set<number>();
const out: number[] = [];
let match = regex.exec(content);
while (match !== null) {
const lineIndex = findLineIndex(starts, match.index);
if (lineIndex >= 0 && !seen.has(lineIndex)) {
seen.add(lineIndex);
out.push(lineIndex);View on GitHub (pinned to 9690622007)
Solutions
- Fix the regex syntax (close groups/brackets, valid quantifier bounds)
- Escape metacharacters in literal text: \( \[ \. etc.
- Pre-test the pattern with new RegExp(pattern, 'i') in a try/catch before calling
Example fix
// before
const pattern = `log(${level}`; // invalid
await grepTool.execute({ pattern, path })
// after
const pattern = `log\(${level}`; // escaped
await grepTool.execute({ pattern, path }) Defensive patterns
Strategy: validation
Validate before calling
function assertValidRegex(pattern: string, flags = "i"): void {
try { new RegExp(pattern, flags); } catch (err) {
throw new Error(`Pattern is not a valid JS RegExp: ${pattern}`, { cause: err });
}
} Type guard
function isValidRegex(pattern: string): boolean {
try { new RegExp(pattern); return true; } catch { return false; }
} Try / catch
try {
return await grepTool.execute({ pattern, path });
} catch (err) {
if (err instanceof ToolError && err.message.startsWith("Invalid regex:")) {
const literal = pattern.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
return await grepTool.execute({ pattern: literal, path }); // retry as escaped literal
}
throw err;
} Prevention
- Escape metacharacters when searching literal text
- Pre-compile with new RegExp(pattern) in a try/catch before calling the tool
- Avoid PCRE/RE2-only syntax; target JS RegExp semantics
When it happens
Trigger: Patterns like 'foo(' , '[unclosed', 'a{2,1}', a stray '*' after nothing to repeat, or invalid backreferences passed as the grep pattern.
Common situations: Unescaped user text used as a regex (parentheses, brackets in prose or code snippets); patterns built by string concatenation; switching between tools where one accepts literal text and this one expects regex.
Related errors
- path entry "${entry}" has an invalid selector ":${internalSp
- path entry "${entry}" — only line-range selectors like ":50-
- Line-range selector requires a single file, not a glob: ${en
- Replacement text is not valid UTF-8: {err}
- err.to_string() (invalid regex pattern)
AI-assisted analysis of can1357/oh-my-pi@9690622007 (2026-08-31).
Data as JSON: /api/errors/4693767678c555cd.
Report an issue: GitHub.