Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions claude-code/bundle/pre-tool-use.js
Original file line number Diff line number Diff line change
Expand Up @@ -639,9 +639,9 @@ function buildPathCondition(targetPath) {
return `(path = '${sqlStr(clean)}' OR path LIKE '${sqlLike(clean)}/%' ESCAPE '\\')`;
}
async function searchDeeplakeTables(api, memoryTable, sessionsTable, opts) {
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns } = opts;
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
const limit = opts.limit ?? 100;
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : [escapedPattern];
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern];
const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);
const memQuery = `SELECT path, summary::text AS content, 0 AS source_order, '' AS creation_date FROM "${memoryTable}" WHERE 1=1${pathFilter}${memFilter} LIMIT ${limit}`;
Expand Down Expand Up @@ -730,13 +730,15 @@ function buildGrepSearchOptions(params, targetPath) {
const hasRegexMeta = !params.fixedString && /[.*+?^${}()|[\]\\]/.test(params.pattern);
const literalPrefilter = hasRegexMeta ? extractRegexLiteralPrefilter(params.pattern) : null;
const alternationPrefilters = hasRegexMeta ? extractRegexAlternationPrefilters(params.pattern) : null;
const multiWordPatterns = !hasRegexMeta ? params.pattern.split(/\s+/).filter((w) => w.length > 2).slice(0, 4) : [];
return {
pathFilter: buildPathFilter(targetPath),
contentScanOnly: hasRegexMeta,
likeOp: params.ignoreCase ? "ILIKE" : "LIKE",
escapedPattern: sqlLike(params.pattern),
prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : void 0,
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal))
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
multiWordPatterns: multiWordPatterns.length > 1 ? multiWordPatterns.map((w) => sqlLike(w)) : void 0
};
}
function buildContentFilter(column, likeOp, patterns) {
Expand Down Expand Up @@ -1951,6 +1953,17 @@ async function processPreToolUse(input, deps = {}) {
const toolPath = getReadTargetPath(input.tool_input) ?? input.tool_input.path ?? "";
if (!shellCmd && (touchesMemory(cmd) || touchesMemory(toolPath))) {
const guidance = "[RETRY REQUIRED] The command you tried is not available for ~/.deeplake/memory/. This virtual filesystem only supports bash builtins: cat, ls, grep, echo, jq, head, tail, sed, awk, wc, sort, find, etc. python, python3, node, and curl are NOT available. You MUST rewrite your command using only the bash tools listed above and try again. For example, to parse JSON use: cat file.json | jq '.key'. To count keys: cat file.json | jq 'keys | length'.";
const isReadLike = /^(?:python3?|node|deno|bun|ruby|perl)\b/.test(cmd.trim());
const hasShellMeta = /[$`;|&<>()\\]/.test(cmd);
if (isReadLike && !hasShellMeta) {
const pathMatch = cmd.match(/~\/\.deeplake\/memory\/[\w./_-]+/) || toolPath.match(/~\/\.deeplake\/memory\/[\w./_-]+/);
const memPath = pathMatch ? pathMatch[0] : "";
const cleanPath = memPath ? rewritePaths(memPath) : "";
if (cleanPath && !cleanPath.endsWith("/")) {
logFn(`unsupported command on file, converting to cat: ${cleanPath}`);
return buildAllowDecision(`cat '${cleanPath.replace(/'/g, "'\\''")}'`, "[DeepLake] converted unsupported interpreter read to cat");
}
}
logFn(`unsupported command, returning guidance: ${cmd}`);
return buildAllowDecision(`echo ${JSON.stringify(guidance)}`, "[DeepLake] unsupported command \u2014 rewrite using bash builtins");
}
Expand Down
8 changes: 5 additions & 3 deletions claude-code/bundle/shell/deeplake-shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -67326,9 +67326,9 @@ function buildPathCondition(targetPath) {
return `(path = '${sqlStr(clean)}' OR path LIKE '${sqlLike(clean)}/%' ESCAPE '\\')`;
}
async function searchDeeplakeTables(api, memoryTable, sessionsTable, opts) {
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns } = opts;
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
const limit = opts.limit ?? 100;
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : [escapedPattern];
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern];
const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);
const memQuery = `SELECT path, summary::text AS content, 0 AS source_order, '' AS creation_date FROM "${memoryTable}" WHERE 1=1${pathFilter}${memFilter} LIMIT ${limit}`;
Expand Down Expand Up @@ -67427,13 +67427,15 @@ function buildGrepSearchOptions(params, targetPath) {
const hasRegexMeta = !params.fixedString && /[.*+?^${}()|[\]\\]/.test(params.pattern);
const literalPrefilter = hasRegexMeta ? extractRegexLiteralPrefilter(params.pattern) : null;
const alternationPrefilters = hasRegexMeta ? extractRegexAlternationPrefilters(params.pattern) : null;
const multiWordPatterns = !hasRegexMeta ? params.pattern.split(/\s+/).filter((w20) => w20.length > 2).slice(0, 4) : [];
return {
pathFilter: buildPathFilter(targetPath),
contentScanOnly: hasRegexMeta,
likeOp: params.ignoreCase ? "ILIKE" : "LIKE",
escapedPattern: sqlLike(params.pattern),
prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : void 0,
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal))
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
multiWordPatterns: multiWordPatterns.length > 1 ? multiWordPatterns.map((w20) => sqlLike(w20)) : void 0
};
}
function buildContentFilter(column, likeOp, patterns) {
Expand Down
8 changes: 5 additions & 3 deletions codex/bundle/pre-tool-use.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,9 +625,9 @@ function buildPathCondition(targetPath) {
return `(path = '${sqlStr(clean)}' OR path LIKE '${sqlLike(clean)}/%' ESCAPE '\\')`;
}
async function searchDeeplakeTables(api, memoryTable, sessionsTable, opts) {
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns } = opts;
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
const limit = opts.limit ?? 100;
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : [escapedPattern];
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern];
const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);
const memQuery = `SELECT path, summary::text AS content, 0 AS source_order, '' AS creation_date FROM "${memoryTable}" WHERE 1=1${pathFilter}${memFilter} LIMIT ${limit}`;
Expand Down Expand Up @@ -716,13 +716,15 @@ function buildGrepSearchOptions(params, targetPath) {
const hasRegexMeta = !params.fixedString && /[.*+?^${}()|[\]\\]/.test(params.pattern);
const literalPrefilter = hasRegexMeta ? extractRegexLiteralPrefilter(params.pattern) : null;
const alternationPrefilters = hasRegexMeta ? extractRegexAlternationPrefilters(params.pattern) : null;
const multiWordPatterns = !hasRegexMeta ? params.pattern.split(/\s+/).filter((w) => w.length > 2).slice(0, 4) : [];
return {
pathFilter: buildPathFilter(targetPath),
contentScanOnly: hasRegexMeta,
likeOp: params.ignoreCase ? "ILIKE" : "LIKE",
escapedPattern: sqlLike(params.pattern),
prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : void 0,
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal))
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
multiWordPatterns: multiWordPatterns.length > 1 ? multiWordPatterns.map((w) => sqlLike(w)) : void 0
};
}
function buildContentFilter(column, likeOp, patterns) {
Expand Down
8 changes: 5 additions & 3 deletions codex/bundle/shell/deeplake-shell.js
Original file line number Diff line number Diff line change
Expand Up @@ -67326,9 +67326,9 @@ function buildPathCondition(targetPath) {
return `(path = '${sqlStr(clean)}' OR path LIKE '${sqlLike(clean)}/%' ESCAPE '\\')`;
}
async function searchDeeplakeTables(api, memoryTable, sessionsTable, opts) {
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns } = opts;
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
const limit = opts.limit ?? 100;
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : [escapedPattern];
const filterPatterns = contentScanOnly ? prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : prefilterPattern ? [prefilterPattern] : [] : multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern];
const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);
const memQuery = `SELECT path, summary::text AS content, 0 AS source_order, '' AS creation_date FROM "${memoryTable}" WHERE 1=1${pathFilter}${memFilter} LIMIT ${limit}`;
Expand Down Expand Up @@ -67427,13 +67427,15 @@ function buildGrepSearchOptions(params, targetPath) {
const hasRegexMeta = !params.fixedString && /[.*+?^${}()|[\]\\]/.test(params.pattern);
const literalPrefilter = hasRegexMeta ? extractRegexLiteralPrefilter(params.pattern) : null;
const alternationPrefilters = hasRegexMeta ? extractRegexAlternationPrefilters(params.pattern) : null;
const multiWordPatterns = !hasRegexMeta ? params.pattern.split(/\s+/).filter((w20) => w20.length > 2).slice(0, 4) : [];
return {
pathFilter: buildPathFilter(targetPath),
contentScanOnly: hasRegexMeta,
likeOp: params.ignoreCase ? "ILIKE" : "LIKE",
escapedPattern: sqlLike(params.pattern),
prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : void 0,
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal))
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
multiWordPatterns: multiWordPatterns.length > 1 ? multiWordPatterns.map((w20) => sqlLike(w20)) : void 0
};
}
function buildContentFilter(column, likeOp, patterns) {
Expand Down
22 changes: 22 additions & 0 deletions src/hooks/pre-tool-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,28 @@ export async function processPreToolUse(input: PreToolUseInput, deps: ClaudePreT
"python, python3, node, and curl are NOT available. " +
"You MUST rewrite your command using only the bash tools listed above and try again. " +
"For example, to parse JSON use: cat file.json | jq '.key'. To count keys: cat file.json | jq 'keys | length'.";

// Fast-path: a clean single-file read attempt by an unsupported interpreter
// (python/node/ruby/perl, no shell metacharacters) gets rewritten to
// `cat '<path>'` so the agent doesn't burn a turn on a RETRY. Anything with
// $(...), backticks, pipes, redirects, or chains falls through to the
// guidance below — safer than trying to rewrite composite commands.
const isReadLike = /^(?:python3?|node|deno|bun|ruby|perl)\b/.test(cmd.trim());
const hasShellMeta = /[$`;|&<>()\\]/.test(cmd);
if (isReadLike && !hasShellMeta) {
const pathMatch = cmd.match(/~\/\.deeplake\/memory\/[\w./_-]+/)
|| toolPath.match(/~\/\.deeplake\/memory\/[\w./_-]+/);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path regex only matches ~/ prefix, so python3 /home/runner/.deeplake/memory/data.json (an absolute path) passes touchesMemory() and isReadLike, but silently falls through to RETRY guidance because neither cmd.match(...) nor toolPath.match(...) fires. Extend the regex to also match the MEMORY_PATH absolute prefix, or use rewritePaths on the whole cmd/toolPath and inspect the result.

Suggested change
|| toolPath.match(/~\/\.deeplake\/memory\/[\w./_-]+/);
const pathMatch = cmd.match(/(?:~\/\.deeplake\/memory|\/[^\s]*\.deeplake\/memory)\/[\w./_-]+/)
|| toolPath.match(/(?:~\/\.deeplake\/memory|\/[^\s]*\.deeplake\/memory)\/[\w./_-]+/);

const memPath = pathMatch ? pathMatch[0] : "";
const cleanPath = memPath ? rewritePaths(memPath) : "";
if (cleanPath && !cleanPath.endsWith("/")) {
logFn(`unsupported command on file, converting to cat: ${cleanPath}`);
return buildAllowDecision(
`cat '${cleanPath.replace(/'/g, "'\\''")}'`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test exercises the new happy path — the existing tests only verify that commands with shell metacharacters still produce RETRY guidance (the negative path). Add a test that python3 ~/.deeplake/memory/data.jsonupdatedCommand contains cat '/data.json' to prevent silent regressions in the rewrite logic.

"[DeepLake] converted unsupported interpreter read to cat",
);
}
}

logFn(`unsupported command, returning guidance: ${cmd}`);
return buildAllowDecision(
`echo ${JSON.stringify(guidance)}`,
Expand Down
15 changes: 13 additions & 2 deletions src/shell/grep-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ export interface SearchOptions {
prefilterPattern?: string;
/** Optional safe literal alternation anchors for regex searches (e.g. foo|bar). */
prefilterPatterns?: string[];
/** Per-word patterns for non-regex multi-word queries (OR-joined). */
multiWordPatterns?: string[];
/** Per-table row cap. */
limit?: number;
}
Expand Down Expand Up @@ -254,11 +256,11 @@ export async function searchDeeplakeTables(
sessionsTable: string,
opts: SearchOptions,
): Promise<ContentRow[]> {
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns } = opts;
const { pathFilter, contentScanOnly, likeOp, escapedPattern, prefilterPattern, prefilterPatterns, multiWordPatterns } = opts;
const limit = opts.limit ?? 100;
const filterPatterns = contentScanOnly
? (prefilterPatterns && prefilterPatterns.length > 0 ? prefilterPatterns : (prefilterPattern ? [prefilterPattern] : []))
: [escapedPattern];
: (multiWordPatterns && multiWordPatterns.length > 1 ? multiWordPatterns : [escapedPattern]);
const memFilter = buildContentFilter("summary::text", likeOp, filterPatterns);
const sessFilter = buildContentFilter("message::text", likeOp, filterPatterns);

Expand Down Expand Up @@ -377,13 +379,22 @@ export function buildGrepSearchOptions(params: GrepMatchParams, targetPath: stri
const hasRegexMeta = !params.fixedString && /[.*+?^${}()|[\]\\]/.test(params.pattern);
const literalPrefilter = hasRegexMeta ? extractRegexLiteralPrefilter(params.pattern) : null;
const alternationPrefilters = hasRegexMeta ? extractRegexAlternationPrefilters(params.pattern) : null;
// For non-regex multi-word patterns, split into per-word OR filters so
// natural-language queries match any token, not only the full phrase.
const multiWordPatterns = (!hasRegexMeta)
? params.pattern.split(/\s+/).filter((w) => w.length > 2).slice(0, 4)
: [];

return {
pathFilter: buildPathFilter(targetPath),
contentScanOnly: hasRegexMeta,
likeOp: params.ignoreCase ? "ILIKE" : "LIKE",
escapedPattern: sqlLike(params.pattern),
prefilterPattern: literalPrefilter ? sqlLike(literalPrefilter) : undefined,
prefilterPatterns: alternationPrefilters?.map((literal) => sqlLike(literal)),
multiWordPatterns: multiWordPatterns.length > 1
? multiWordPatterns.map((w) => sqlLike(w))
: undefined,
};
}

Expand Down
Loading