From be0faec1e4404ae92a794f84d460438955b59cad Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Wed, 8 Apr 2026 03:35:22 +1000 Subject: [PATCH 1/7] Exclude .git from Find results by default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .git was the most common noise in Find results — hundreds of object files and refs that are never useful. Added to the default exclude list alongside dist and node_modules. Description updated to match. The input example already showed .git in an explicit exclude list, which is now redundant but harmless. Partial #210 --- packages/claude-sdk-tools/src/Find/Find.ts | 2 +- packages/claude-sdk-tools/src/Find/schema.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/claude-sdk-tools/src/Find/Find.ts b/packages/claude-sdk-tools/src/Find/Find.ts index f67a5c0..e7c2b86 100644 --- a/packages/claude-sdk-tools/src/Find/Find.ts +++ b/packages/claude-sdk-tools/src/Find/Find.ts @@ -9,7 +9,7 @@ export function createFind(fs: IFileSystem) { return defineTool({ operation: 'read', name: 'Find', - description: 'Find files or directories. Excludes node_modules and dist by default. Output can be piped into Grep.', + description: 'Find files or directories. Excludes node_modules, dist and .git by default. Output can be piped into Grep.', input_schema: FindInputSchema, input_examples: [{ path: '.' }, { path: './src', pattern: '*.ts' }, { path: '.', type: 'directory' }, { path: '.', pattern: '*.ts', exclude: ['dist', 'node_modules', '.git'] }], handler: async (input) => { diff --git a/packages/claude-sdk-tools/src/Find/schema.ts b/packages/claude-sdk-tools/src/Find/schema.ts index ca4d2c3..a6ac054 100644 --- a/packages/claude-sdk-tools/src/Find/schema.ts +++ b/packages/claude-sdk-tools/src/Find/schema.ts @@ -15,6 +15,6 @@ export const FindInputSchema = z.object({ path: z.string().describe('Directory to search. Supports absolute, relative, ~ and $HOME.'), pattern: z.string().optional().describe('Glob pattern to match filenames, e.g. *.ts, *.{ts,js}'), type: z.enum(['file', 'directory', 'both']).default('file').describe('Whether to find files, directories, or both'), - exclude: z.array(z.string()).default(['dist', 'node_modules']).describe('Directory names to exclude from search'), + exclude: z.array(z.string()).default(['dist', 'node_modules', '.git']).describe('Directory names to exclude from search'), maxDepth: z.number().int().min(1).optional().describe('Maximum directory depth to search'), }); From 823c31a0a8a22d93bcfbca4488e329872bdc055c Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Wed, 8 Apr 2026 03:42:04 +1000 Subject: [PATCH 2/7] Switch Find pattern field from glob to regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The glob implementation was minimal and broken for common cases — *.{ts,js} didn't work because {} got regex-escaped rather than treated as alternation. Replacing it with regex makes Find consistent with Grep and SearchFiles, and actually works. Pattern is tested against the filename (basename) via RegExp.test(). matchGlob.ts deleted as it is no longer referenced. Partial #210 --- packages/claude-sdk-tools/src/Find/Find.ts | 2 +- packages/claude-sdk-tools/src/Find/schema.ts | 2 +- packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts | 5 ++--- packages/claude-sdk-tools/src/fs/NodeFileSystem.ts | 5 ++--- packages/claude-sdk-tools/src/fs/matchGlob.ts | 9 --------- packages/claude-sdk-tools/test/Find.spec.ts | 8 ++++---- 6 files changed, 10 insertions(+), 21 deletions(-) delete mode 100644 packages/claude-sdk-tools/src/fs/matchGlob.ts diff --git a/packages/claude-sdk-tools/src/Find/Find.ts b/packages/claude-sdk-tools/src/Find/Find.ts index e7c2b86..4a76af8 100644 --- a/packages/claude-sdk-tools/src/Find/Find.ts +++ b/packages/claude-sdk-tools/src/Find/Find.ts @@ -11,7 +11,7 @@ export function createFind(fs: IFileSystem) { name: 'Find', description: 'Find files or directories. Excludes node_modules, dist and .git by default. Output can be piped into Grep.', input_schema: FindInputSchema, - input_examples: [{ path: '.' }, { path: './src', pattern: '*.ts' }, { path: '.', type: 'directory' }, { path: '.', pattern: '*.ts', exclude: ['dist', 'node_modules', '.git'] }], + input_examples: [{ path: '.' }, { path: './src', pattern: '\.ts$' }, { path: '.', type: 'directory' }, { path: '.', pattern: '\.(ts|js)$' }], handler: async (input) => { const dir = expandPath(input.path, fs); let paths: string[]; diff --git a/packages/claude-sdk-tools/src/Find/schema.ts b/packages/claude-sdk-tools/src/Find/schema.ts index a6ac054..8e71c54 100644 --- a/packages/claude-sdk-tools/src/Find/schema.ts +++ b/packages/claude-sdk-tools/src/Find/schema.ts @@ -13,7 +13,7 @@ export const FindOutputSchema = z.union([FindOutputSuccessSchema, FindOutputFail export const FindInputSchema = z.object({ path: z.string().describe('Directory to search. Supports absolute, relative, ~ and $HOME.'), - pattern: z.string().optional().describe('Glob pattern to match filenames, e.g. *.ts, *.{ts,js}'), + pattern: z.string().optional().describe('Regex pattern to match filenames, e.g. \\.ts$, \\.(ts|js)$'), type: z.enum(['file', 'directory', 'both']).default('file').describe('Whether to find files, directories, or both'), exclude: z.array(z.string()).default(['dist', 'node_modules', '.git']).describe('Directory names to exclude from search'), maxDepth: z.number().int().min(1).optional().describe('Maximum directory depth to search'), diff --git a/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts b/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts index a26c938..7951357 100644 --- a/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts +++ b/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts @@ -1,5 +1,4 @@ import type { FindOptions, IFileSystem, StatResult } from './IFileSystem'; -import { matchGlob } from './matchGlob'; /** * In-memory filesystem implementation for testing. @@ -127,7 +126,7 @@ export class MemoryFileSystem implements IFileSystem { if (type === 'file' || type === 'both') { const fileName = parts[parts.length - 1]; - if (!pattern || matchGlob(pattern, fileName)) { + if (!pattern || new RegExp(pattern).test(fileName)) { results.push(filePath); } } @@ -136,7 +135,7 @@ export class MemoryFileSystem implements IFileSystem { if (type === 'directory' || type === 'both') { for (const dir of dirs) { const dirName = dir.split('/').pop() ?? ''; - if (!pattern || matchGlob(pattern, dirName)) { + if (!pattern || new RegExp(pattern).test(dirName)) { results.push(dir); } } diff --git a/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts b/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts index abc8d1d..232d1ee 100644 --- a/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts +++ b/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts @@ -3,7 +3,6 @@ import { mkdir, readdir, readFile, rm, rmdir, stat, writeFile } from 'node:fs/pr import { homedir as osHomedir } from 'node:os'; import { dirname, join } from 'node:path'; import type { FindOptions, IFileSystem, StatResult } from './IFileSystem'; -import { matchGlob } from './matchGlob'; /** * Production filesystem implementation using Node.js fs APIs. @@ -63,14 +62,14 @@ async function walk(dir: string, options: FindOptions, depth: number): Promise { expect(values).toContain('/src/components/Button.tsx'); }); - it('filters by glob pattern', async () => { + it('filters by regex pattern', async () => { const Find = createFind(makeFs()); - const result = await call(Find, { path: '/src', pattern: '*.ts' }); + const result = await call(Find, { path: '/src', pattern: '\.ts$' }); const { values } = result as { type: 'files'; values: string[] }; expect(values).toContain('/src/index.ts'); expect(values).toContain('/src/utils.ts'); @@ -49,9 +49,9 @@ describe('createFind u2014 file results', () => { expect(values).toContain('/src/index.ts'); }); - it('** glob pattern matches files in subdirectories', async () => { + it('regex pattern matches files in subdirectories', async () => { const Find = createFind(makeFs()); - const result = await call(Find, { path: '/', pattern: '**/*.ts' }); + const result = await call(Find, { path: '/', pattern: '\.ts$' }); const { values } = result as { type: 'files'; values: string[] }; expect(values).toContain('/src/index.ts'); expect(values).toContain('/src/utils.ts'); From 5b3a82b25929048c4908d6262862dfa053f2ca8e Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Wed, 8 Apr 2026 14:01:06 +1000 Subject: [PATCH 3/7] Test that .git is excluded from Find results by default The default exclude list was extended to include .git in the PR that added this change, but no test covered it. The test creates a filesystem with .git internals alongside real source files and verifies Find returns the source file but none of the .git paths. --- packages/claude-sdk-tools/test/Find.spec.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/claude-sdk-tools/test/Find.spec.ts b/packages/claude-sdk-tools/test/Find.spec.ts index f90e38d..33e386e 100644 --- a/packages/claude-sdk-tools/test/Find.spec.ts +++ b/packages/claude-sdk-tools/test/Find.spec.ts @@ -49,6 +49,20 @@ describe('createFind u2014 file results', () => { expect(values).toContain('/src/index.ts'); }); + it('excludes .git by default', async () => { + const fs = new MemoryFileSystem({ + '/src/index.ts': 'export const x = 1;', + '/.git/config': '[core]', + '/.git/HEAD': 'ref: refs/heads/main', + '/.git/objects/ab/cdef': 'blob', + }); + const Find = createFind(fs); + const result = await call(Find, { path: '/' }); + const { values } = result as { type: 'files'; values: string[] }; + expect(values).toContain('/src/index.ts'); + expect(values.some((v) => v.startsWith('/.git/'))).toBe(false); + }); + it('regex pattern matches files in subdirectories', async () => { const Find = createFind(makeFs()); const result = await call(Find, { path: '/', pattern: '\.ts$' }); From 3c49ffa08b8933fd0fe4ae1a8f16ae5b86869686 Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Wed, 8 Apr 2026 14:02:32 +1000 Subject: [PATCH 4/7] Use actual/expected/toEqual in .git exclusion test --- packages/claude-sdk-tools/test/Find.spec.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/claude-sdk-tools/test/Find.spec.ts b/packages/claude-sdk-tools/test/Find.spec.ts index 33e386e..f0c37ea 100644 --- a/packages/claude-sdk-tools/test/Find.spec.ts +++ b/packages/claude-sdk-tools/test/Find.spec.ts @@ -57,10 +57,9 @@ describe('createFind u2014 file results', () => { '/.git/objects/ab/cdef': 'blob', }); const Find = createFind(fs); - const result = await call(Find, { path: '/' }); - const { values } = result as { type: 'files'; values: string[] }; - expect(values).toContain('/src/index.ts'); - expect(values.some((v) => v.startsWith('/.git/'))).toBe(false); + const actual = ((await call(Find, { path: '/' })) as { type: 'files'; values: string[] }).values; + const expected = ['/src/index.ts']; + expect(actual).toEqual(expected); }); it('regex pattern matches files in subdirectories', async () => { From 0e0f65cfd2dbe5635efdeacd79fc7a8cde7dff22 Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Wed, 8 Apr 2026 14:05:25 +1000 Subject: [PATCH 5/7] =?UTF-8?q?Remove=20unnecessary=20cast=20=E2=80=94=20c?= =?UTF-8?q?all()=20already=20infers=20the=20output=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/claude-sdk-tools/test/Find.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/claude-sdk-tools/test/Find.spec.ts b/packages/claude-sdk-tools/test/Find.spec.ts index f0c37ea..2d9aa3a 100644 --- a/packages/claude-sdk-tools/test/Find.spec.ts +++ b/packages/claude-sdk-tools/test/Find.spec.ts @@ -57,7 +57,8 @@ describe('createFind u2014 file results', () => { '/.git/objects/ab/cdef': 'blob', }); const Find = createFind(fs); - const actual = ((await call(Find, { path: '/' })) as { type: 'files'; values: string[] }).values; + const result = await call(Find, { path: '/' }); + const actual = result.values; const expected = ['/src/index.ts']; expect(actual).toEqual(expected); }); From 6a5d240fb395c7477d565a166e6a60b8ded939d3 Mon Sep 17 00:00:00 2001 From: Stephen Hellicar Date: Wed, 8 Apr 2026 14:07:46 +1000 Subject: [PATCH 6/7] Compile pattern regex once per find call, not per entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both NodeFileSystem and MemoryFileSystem were calling new RegExp(pattern) for every file and directory entry during the walk. NodeFileSystem is worse because walk() is recursive — the regex was being recompiled at every depth level for every entry. Fix: compile once in find() before the walk begins, pass the RegExp through as a parameter. NodeFileSystem threads it through the recursive walk() signature. MemoryFileSystem computes it once before the loop. --- .../claude-sdk-tools/src/fs/MemoryFileSystem.ts | 5 +++-- packages/claude-sdk-tools/src/fs/NodeFileSystem.ts | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts b/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts index 7951357..fa237e1 100644 --- a/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts +++ b/packages/claude-sdk-tools/src/fs/MemoryFileSystem.ts @@ -94,6 +94,7 @@ export class MemoryFileSystem implements IFileSystem { throw err; } + const re = pattern ? new RegExp(pattern) : undefined; const results: string[] = []; const dirs = new Set(); @@ -126,7 +127,7 @@ export class MemoryFileSystem implements IFileSystem { if (type === 'file' || type === 'both') { const fileName = parts[parts.length - 1]; - if (!pattern || new RegExp(pattern).test(fileName)) { + if (!re || re.test(fileName)) { results.push(filePath); } } @@ -135,7 +136,7 @@ export class MemoryFileSystem implements IFileSystem { if (type === 'directory' || type === 'both') { for (const dir of dirs) { const dirName = dir.split('/').pop() ?? ''; - if (!pattern || new RegExp(pattern).test(dirName)) { + if (!re || re.test(dirName)) { results.push(dir); } } diff --git a/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts b/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts index 232d1ee..aa9d1dd 100644 --- a/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts +++ b/packages/claude-sdk-tools/src/fs/NodeFileSystem.ts @@ -34,7 +34,8 @@ export class NodeFileSystem implements IFileSystem { } public async find(path: string, options?: FindOptions): Promise { - return walk(path, options ?? {}, 1); + const re = options?.pattern ? new RegExp(options.pattern) : undefined; + return walk(path, options ?? {}, 1, re); } public async stat(path: string): Promise { @@ -43,8 +44,8 @@ export class NodeFileSystem implements IFileSystem { } } -async function walk(dir: string, options: FindOptions, depth: number): Promise { - const { maxDepth, exclude = [], pattern, type = 'file' } = options; +async function walk(dir: string, options: FindOptions, depth: number, re: RegExp | undefined): Promise { + const { maxDepth, exclude = [], type = 'file' } = options; if (maxDepth !== undefined && depth > maxDepth) { return []; @@ -62,14 +63,14 @@ async function walk(dir: string, options: FindOptions, depth: number): Promise Date: Wed, 8 Apr 2026 14:36:12 +1000 Subject: [PATCH 7/7] Add test for previous glob usage. --- packages/claude-sdk-tools/test/Find.spec.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/claude-sdk-tools/test/Find.spec.ts b/packages/claude-sdk-tools/test/Find.spec.ts index 2d9aa3a..4e28f1d 100644 --- a/packages/claude-sdk-tools/test/Find.spec.ts +++ b/packages/claude-sdk-tools/test/Find.spec.ts @@ -32,6 +32,14 @@ describe('createFind u2014 file results', () => { expect(values).not.toContain('/src/components/Button.tsx'); }); + it('throws on glob pattern', async () => { + const Find = createFind(makeFs()); + + const actual = async () => await call(Find, { path: '/src', pattern: '*.ts' }); + + await expect(actual).rejects.toThrow(SyntaxError); + }); + it('respects maxDepth', async () => { const Find = createFind(makeFs()); const result = await call(Find, { path: '/src', maxDepth: 1 });