file-search-on
Content-type aware file search with CEL-powered attribute filtering.
file-search-on walks a directory tree and returns files matching a CEL expression evaluated over each file's metadata and content-type-specific attributes. Instead of grepping by name, ask things like:
file-search-on 'is_pdf && page_count > 10 && author == "Jane Doe"'
file-search-on 'is_image && gps_lat > 51.4 && gps_lat < 51.6' # photos near home
file-search-on 'is_audio && artist == "Radiohead" && year < 2000'
file-search-on 'is_video && video_height >= 2160 && video_codec == "h265"'
file-search-on 'is_office && language == "fr"'
file-search-on 'is_markdown && "longread" in tags && word_count > 1000'
# Or match fuzzily — typos in the data are no longer fatal:
file-search-on 'is_audio && levenshtein(artist, "Radiohead") <= 2' # catches "Radiohad", "Radiohea"
file-search-on 'is_image && soundex(camera_make) == soundex("Nikon")' # phonetic match across capitalisation / spelling
file-search-on 'is_markdown && ngram_similarity(title, "kubernetes", 2) > 0.6' # substring-tolerant title match
Across 74 file formats organised into thirteen content-type families (documents, data, images, audio, video, office, ebooks, plain text, archives, compiled binaries, email, source code, notebooks), with format-specific metadata extraction.
Built in the open — issues, PRs, and feature requests warmly welcomed. See Contributing.
Quick start
Install with Homebrew, then hand the search tools to Claude Code — two commands:
# 1. Install the binary (macOS / Linux)
brew install richardwooding/tap/file-search-on
# 2. Register it as an MCP server in Claude Code
claude mcp add file-search-on -- file-search-on mcp
That's it. Claude Code can now query your files by typed content-type attributes — ask it things like "find every PDF over 10 pages I haven't opened this year", "which Go files have the highest git churn?", or "are there any AWS keys in this repo?" and it drives the MCP tools behind the scenes — including a cross-file code graph (imported_by, find_definition, who_calls, calls, dead_code) for "who depends on X?", "where is Y defined?", "who calls Z?", and "what does Z call?".
- Make it available in every project (not just the current one) with the user scope:
claude mcp add -s user file-search-on -- file-search-on mcp. - Confirm the connection with
claude mcp list, or/mcpinside a Claude Code session.
Prefer the command line? The same binary is a standalone CLI — file-search-on 'is_pdf && page_count > 10'. See Usage. Other install methods (Docker, pre-built binaries, go install) are under Install.
Features
-
Pluggable content-type detection — extension-first with magic-byte fallback. New formats are a single registration call.
-
Thirteen content-type families, each with its own metadata extractors:
Family Formats Bundle of attributes Documents PDF, EPUB title, author, language, page_count Markup Markdown, HTML, XML title, word_count, frontmatter, language, root_element Data JSON, YAML, TOML, CSV, TSV json_kind, yaml_kind, yaml_document_count, column_count, csv_columns Plain text TXT, log, … line_count, word_count Images JPEG, PNG, GIF, WebP, TIFF, BMP, SVG, HEIC, RAW (Canon CR2 / CR3, Nikon NEF, Sony ARW, Adobe DNG, Fujifilm RAF, Olympus ORF, Panasonic RW2) — predicates is_raw_photo,is_cr2,is_cr3,is_nef,is_arw,is_dng,is_raf,is_orf,is_rw2. HEIC + sibling MOV → Apple Live Photo pairing (is_live_photo,is_live_photo_video).dimensions + EXIF: camera, lens, GPS, ISO, focal_length, taken_at; RAW adds raw_kind,raw_vendor; Live Photo addslive_photo_video_path,live_photo_video_size,live_photo_image_pathAudio MP3, M4A, FLAC, OGG, WAV tags (artist, album, genre, year, …) + duration, bitrate / nominal_bitrate, sample_rate, channels, bit_depth, ReplayGain Video MP4, MOV, MKV, WebM, AVI duration, bitrate / nominal_bitrate, video_codec, audio_codec, video_width/height, frame_rate, rotation, HDR / colour-space, subtitles Office DOCX, XLSX, PPTX, ODT title, author, language (Dublin Core) Archives ZIP (incl. JAR / WAR / EAR), TAR, TAR.GZ, GZIP entry_count, uncompressed_size, top_level_entries, has_root_dir Binaries ELF (Linux/BSD), Mach-O (macOS, incl. universal), PE (Windows). Mach-O code signature parsing surfaces team ID + entitlements. architectures, bitness, binary_format, binary_type, is_dynamically_linked, is_stripped, entry_point, is_codesigned, is_apple_signed, is_third_party_signed, codesign_identifier, codesign_team_id, codesign_hash_type, codesign_hardened_runtime, codesign_library_validation, codesign_killed, codesign_adhoc, entitlements, entitlement_app_sandbox, entitlement_full_disk_access, entitlement_network_client, entitlement_network_server Email RFC 5322 ( .eml), Unix mbox (.mbox)title (subject), author (from), email_to, email_cc, sent_at, attachment_count, email_count Source code Go, Python, JS/TS, Rust, C/C++, Java, Ruby, Swift, Kotlin, Scala, Shell, Lua, Elixir, Clojure, Haskell, OCaml, Zig, C#, PHP, Perl, R, Ada, SQL, Visual Basic, Fortran, MATLAB, Assembly, Pascal/Delphi language, line_count, loc, comment_loc, blank_loc, functions/type_names/imports/references(Go via stdlib AST; 15 other languages — Python / Java / C# / PHP / Perl / R / MATLAB / Scala / Rust / TypeScript / JavaScript / Ruby / Swift / Kotlin / C / C++ — via embedded tree-sitter),max_complexity(per-file hotspot signal)Notebooks Jupyter .ipynb, Apache Zeppelin.zplncell_count, code_cell_count, markdown_cell_count, kernel, language, title Disk images DMG (UDIF), ISO 9660, VHD, VHDX, VMDK (sparse), QCOW2, WIM disk_image_format, virtual_size, disk_type, volume_label, disk_image_created_at, cluster_bits, is_encrypted, image_count Install packages macOS .pkg(XAR), Debian.deb, Red Hat.rpm, Linux.appimagepackage_format, package_name, package_version, package_release, package_arch, package_kind, appimage_version VM bytecode Java .class(JVM), Python.pyc/.pyo, WebAssembly.wasmbytecode_format, runtime_version, class_name (JVM), super_class (JVM), interfaces (JVM), method_count (JVM), field_count (JVM), access_flags (JVM), python_version, source_mtime, wasm_version, section_count, import_count, export_count Science data FITS (Flexible Image Transport System), VOTable (IVOA astronomical tables), HDF5 (Hierarchical Data Format v5 — LSST, LIGO, NetCDF4, scientific simulations), PDS3 + PDS4 (NASA Planetary Data System — Voyager, Mars rovers, Perseverance, Lucy), CDF (NASA Common Data Format — heliophysics: ACE, Wind, MMS, Parker Solar Probe) science_format, telescope, instrument, object (→ title), observer (→ author), date_obs (→ taken_at), exptime, filter, airmass, ra, dec, bitpix, naxis, naxis1, naxis2, hdu_count, fits_kind, votable_version, table_count, total_rows, field_names, field_units, field_ucds, votable_data_format, hdf5_format_version, hdf5_size_of_offsets, hdf5_size_of_lengths, pds_version, mission_name, spacecraft_name, instrument_name, target_name, product_id, start_time (→ taken_at), cdf_version, cdf_encoding, cdf_majority, variable_count, attribute_count Databases SQLite v3 + WAL / SHM sidecars + FTS3/4/5 body extraction (the most-deployed database in the world — every iOS / Android app, every browser, every CLI with a local store) database_format, sqlite_page_size, sqlite_format_version, sqlite_page_count, sqlite_schema_version, sqlite_text_encoding, sqlite_user_version, sqlite_application_id, sqlite_application_name, sqlite_fts_table_count, sqlite_fts_table_names, sqlite_wal_format_version, sqlite_wal_page_size, sqlite_wal_checkpoint_seq, sqlite_wal_frame_count, sqlite_wal_byte_order Apple property lists Binary ( bplist00) + XML.plist— Info.plist, LaunchAgents, LaunchDaemons, Preferences,.weblocplist_format, plist_root_kind, plist_kind, plist_bundle_identifier, plist_bundle_name, plist_bundle_version, plist_bundle_short_version, plist_executable, plist_min_os_version, plist_label, plist_program, plist_program_arguments, plist_run_at_load, plist_keep_alive Browser bookmarks Chromium-family (Chrome / Brave / Edge / Chromium / Opera / Vivaldi / Arc) BookmarksJSON + SafariBookmarks.plistbookmark_count, bookmark_folder_count, bookmark_folders, bookmark_urls, bookmark_titles, browser_vendor, bookmark_profile Chat exports Slack workspace exports, Discord (DiscordChatExporter) dumps, signal-cli --json— detected by JSON shape (is_chat_export/is_slack_export/is_discord_export/is_signal_export)chat_message_count, chat_participants, chat_channel, chat_workspace, chat_start_at, chat_end_at Fonts TTF, OTF, TTC / OTC collections, WOFF1, WOFF2 (brotli decompression — full attribute extraction) font_format, font_outline_kind, font_family, font_subfamily, font_full_name, font_version, font_postscript_name, font_manufacturer, font_designer, font_license, font_license_url, font_typographic_family, font_weight, font_width, font_embedding, font_panose, font_unicode_ranges, font_revision, font_units_per_em, font_mac_style, font_italic_angle, font_glyph_count, font_axis_count, font_axes, font_collection_count, font_collection_families, woff2_total_sfnt_size, woff2_total_compressed_size 3D models STL (ASCII + binary), Wavefront OBJ, glTF 2.0 ( .gltf+.glb) — predicatesis_3d_model,is_stl,is_obj,is_gltfmodel3d_format, vertex_count, face_count, has_normals, has_textures, materials, bounding_box Type predicates (
is_pdf,is_image,is_audio,is_video,is_office,is_epub, …) light up automatically from the registered content type. See examples/ for recipes by family. -
Exact-name content types for common repo files —
Dockerfile,Makefile,LICENSE,.gitignore,go.mod,package.json,Cargo.toml,Pipfile,Gemfile,requirements.txt,Procfile,Vagrantfile, and more — with per-type predicates (is_dockerfile,is_gomod,is_node_manifest, …) plus family predicates (is_build,is_repo_meta,is_ignore,is_manifest,is_platform). Predicates cross-fire:package.jsonis bothis_node_manifestandis_json. See examples/repo-files.md. -
OS-generated metadata files —
.DS_Store/.localized(macOS),Thumbs.db/Desktop.ini(Windows),.directory(KDE) — with per-type predicates (is_ds_store,is_localized,is_thumbs_db,is_desktop_ini,is_kde_directory), OS-specific family predicates (is_macos_metadata,is_windows_metadata,is_linux_metadata), and the cross-OSis_system_metadata. Lets agents answer "find every macOS leftover under~/Code" or "what platform-cruft is in this archive?" in one query. -
Apple property lists (
.plist) — binary (bplist00) and XML variants. Surfacesis_plistplus a typed attribute set (plist_format,plist_root_kind,plist_kind,plist_bundle_identifier,plist_bundle_name,plist_bundle_version,plist_bundle_short_version,plist_executable,plist_min_os_version,plist_label,plist_program,plist_program_arguments,plist_run_at_load,plist_keep_alive). Path-basedplist_kindregistry labels Info.plist / LaunchAgents / LaunchDaemons / Preferences / .webloc files. Lets agents answer "which LaunchAgents run on login?", "what apps require macOS 14+?", or "find the Info.plist forcom.example.bundle" in one query. -
Browser bookmarks — Chromium-family
Bookmarks(Chrome / Brave / Edge / Chromium / Opera / Vivaldi / Arc) and SafariBookmarks.plist. Surfacesis_bookmark_file/is_chromium_bookmarks/is_safari_bookmarksplusbookmark_count,bookmark_folder_count,bookmark_folders,bookmark_urls,bookmark_titles,browser_vendor(chrome / chromium / edge / brave / opera / vivaldi / arc / safari), andbookmark_profile. With--body, thebodyCEL variable carriestitle\turllines sobody.contains("kubernetes")answers "did I bookmark anything about kubernetes?" across every profile in one query. -
Chat exports — offline Slack workspace exports, Discord (DiscordChatExporter) JSON dumps, and signal-cli
--jsonoutput. All three are plain.jsonfiles with arbitrary names, so they're detected by a streaming top-level-JSON-shape discriminator rather than by extension. Surfacesis_chat_exportplus per-formatis_slack_export/is_discord_export/is_signal_export, and a shared attribute set:chat_message_count,chat_participants(distinct authors),chat_channel,chat_workspace(guild for Discord; empty for Signal),chat_start_at, andchat_end_at. With--body, thebodyCEL variable carries one{timestamp}\t{author}\t{text}line per message sois_chat_export && body.contains("kubernetes")greps the conversation text across an entire export. See examples/chat-exports.md. -
Screenshot OCR —
--ocrruns OCR overimage/*files via the registered provider (macOS Vision today; Linux Tesseract / Windows.Media.Ocr deferred under the same hook). The recognized text populates thebodyCEL variable sobody.contains("kubernetes")queries work over~/Desktopscreenshots the same way they do over markdown files. Plus three new attributes:ocr_confidence(0..1 average across recognized lines),ocr_language(BCP-47 dominant language),ocr_provider(registered engine name). On macOS the OCR helper is bundled in the Homebrew cask; for local devmake ocr-helperbuilds it. On platforms without a registered provider,--ocris a clean no-op. Cached in the body cache (bodies_v1) so subsequent walks are free. See examples/ocr.md. -
Fonts — TrueType (
.ttf), OpenType (.otf), TTC / OTC collections, WOFF1 (.woff), and WOFF2 (.woff2). WOFF2 attribute extraction runs the brotli decompression hop, then slices the metadata tables (name/OS/2/head/post/maxp/fvar) from the decompressed stream and dispatches to the same per-table decoders as the bare-sfnt path —font_family,font_designer,font_weight,font_axesall populate for.woff2collections in modern frontend projects. Surfaces format-family predicates (is_font,is_ttf,is_otf,is_font_collection,is_woff,is_woff2) plus trait predicates (is_variable_font,is_color_font,is_monospace_font,is_italic_font,is_bold_font). Extracted attributes cover thenametable (family, designer, version, manufacturer, license),OS/2(weight, width, embedding permissions, panose, Unicode ranges),head(revision, units-per-em, mac style),post(italic angle),maxp(glyph count), andfvar(variable-font axes —wght/wdth/slnt/ital/opsz). Lets agents answer "find every variable font with an optical-size axis", "license audit — fonts without OFL", or "find Adobe-designed bold fonts" in one query. See examples/fonts.md. -
Project-type detection —
detect-project/find-projects/which-projectsubcommands identify Go / Node / Rust / Python / Ruby / Java / .NET / PHP / Swift / Scala (sbt + Mill) / CMake / Autotools / R / Zig / Perl / MATLAB / Terraform / Docker Compose / Hugo / Jekyll / Eleventy / Astro / Gatsby / MkDocs / Docusaurus / Pelican projects (28 built-in types: 8 SSG + 20 others). Pair with--resolve-projects(file-levelproject_typefilter) and--prune-build-artefacts(skipvendor/node_modules/target/__pycache__/public/_siteetc. automatically). Theis_static_siteCEL predicate addresses any SSG as a group. Define custom project types via CEL in YAML — see examples/projects.md. -
First-class Markdown front-matter — YAML (
---), TOML (+++), and JSON ({ ... }) are recognised by leading bytes. Common keys (title,author,language,tags,categories,draft,date) become top-level CEL variables; everything else lives in a genericfrontmattermap. See examples/markdown.md. -
CEL expressions — the full Common Expression Language: comparisons,
&&/||, string functions, list membership, timestamp arithmetic. Composes naturally with structural attributes. -
Fuzzy, phonetic, and geographic matching — built-in
levenshtein,soundex,ngrams,ngram_similarity, andpoint_in_polygon(for GPS bboxes / city outlines) let you write typo-tolerant and "sounds-like" queries against any string attribute. EXIF camera make inNikkoninstead ofNikon? Artist tag mistyped asRadiohad? Same query catches all of them. See examples/fuzzy-search.md. -
Multiple output formats —
bare(paths only),default,verbose(multi-line),json(NDJSON), or a Gotext/templatevia--format. -
MCP server mode — same binary doubles as a Model Context Protocol server (stdio, HTTP, or SSE). Forty-three tools exposed:
search,search_semantic,read_attributes,read_lines,stats,find_duplicates,find_near_duplicates,find_duplicate_functions,diff_trees,api_diff,find_matches,imported_by,find_definition,references,code_graph,who_calls,calls,trace,impact,call_path,dead_code,test_gaps,coverage_gaps,complexity,coupling,circular,review,unused_exports,churn_owners,watch_search,list_archive_contents,read_file_in_archive,detect_project,find_projects,resolve_project_for_path,list_attributes,validate_expr,list_presets,query_preset,list_embedding_models,pull_embedding_model,index_stats,monitor_info. -
Pure Go, no CGO — cross-compiles cleanly to all six release targets. No image/audio/video decoder dependencies.
-
Parallel walking — files are evaluated across a worker pool (defaults to
NumCPU).
Install
Homebrew (macOS / Linux)
brew install richardwooding/tap/file-search-on
The cask is published from this repo on every tagged release to richardwooding/homebrew-tap.
macOS note: the binary isn't signed with an Apple Developer ID (yet — happy to accept a sponsor!). The Homebrew cask's post-install hook strips the quarantine xattr automatically. If macOS still blocks it on first run:
sudo xattr -dr com.apple.quarantine $(brew --prefix)/bin/file-search-on
Container (Docker / Podman)
OCI images are published to GitHub Container Registry on every tag, with linux/amd64 and linux/arm64 manifests:
docker run --rm -v "$PWD:/work" ghcr.io/richardwooding/file-search-on:latest \
'is_markdown && draft' -d /work
Pin to a specific version with :vX.Y.Z. The base image is cgr.dev/chainguard/static, so the container has the binary and nothing else (no shell).
Pre-built binaries
Pre-built archives for Linux, macOS, and Windows on amd64 and arm64 are attached to every GitHub Release, along with a checksums.txt you should verify.
From source
Requires Go 1.26.2 or newer.
go install github.com/richardwooding/file-search-on/cmd/file-search-on@latest
Or build from a clone:
git clone https://github.com/richardwooding/file-search-on.git
cd file-search-on
go build -o file-search-on ./cmd/file-search-on
Usage
search is the default subcommand. Pass a CEL expression and a directory:
file-search-on 'is_markdown && word_count > 500' -d ./docs
file-search-on 'is_image && iso > 1600' -d ~/Pictures -o json
file-search-on 'is_video && duration > 1800 && video_height >= 2160' -d ~/Movies
file-search-on -d . # empty expression matches every file
Subcommands
| Command | Purpose | Deep dive |
|---|---|---|
search (default) |
CEL expression over file metadata | every page in examples/ |
preset [name] |
Run a named search recipe — recent_changes, large_files, suspicious_files, etc. Without args, lists all presets. |
examples/presets.md |
attrs <path> |
Print attributes for one file (no walk, no CEL) | examples/cookbook.md |
stats [expr] |
Histogram + totals, bucketed by group_by |
examples/group-by.md |
duplicates [expr] |
Byte-identical files by sha256 | examples/duplicates.md |
near-duplicates [expr] |
Similar files by SimHash fingerprint of extracted body | examples/near-duplicates.md |
duplicate-functions [expr] |
Clusters of near-identical functions across the tree (copy-pasted logic file-level scans miss) — your extract-this-helper worklist | examples/source-code.md |
archive-contents <path> [--expr] |
List or filter entries inside ZIP / TAR / TAR.GZ / GZIP — full CEL vocabulary on per-entry attributes | examples/archive-search.md |
archive-read <path> <entry> |
Read a single entry's bytes out of an archive without extracting | examples/archive-search.md |
find-matches <re> --expr <cel> -C N |
Line-level regex hits with context | examples/find-matches.md |
validate <expr> |
Compile-check a CEL expression without walking — fast pre-flight / CI gate; exits non-zero on error with a "did you mean" hint | — |
find-definition <symbol> |
Locate where a function or type is defined across a tree (symbol-aware) | examples/source-code.md |
references <symbol> |
Every usage of a symbol with file:line — calls, type usages, value passing (the IDE "find references" primitive) | examples/source-code.md |
who-calls <symbol> |
Files / functions that call or reference a given symbol (one hop) | examples/source-code.md |
calls <symbol> |
The distinct functions a given function calls (forward lookup; complement to who-calls) |
examples/source-code.md |
call-path <from> <to> |
Shortest call path A→B — how A reaches B through the call graph | examples/source-code.md |
trace <symbol> |
Both directions at once — a symbol's callers + callees (and optional transitive caller closure via --impact-depth) in one view |
examples/source-code.md |
impact <symbol> |
Transitive reverse-dependency closure — the blast radius of changing a function | examples/source-code.md |
imported-by <module> |
Reverse-dependency lookup: every source file that imports a module (--mode exact|prefix|regex) |
examples/source-code.md |
code-graph [expr] |
Project-wide structure overview — import hubs, high-fan-out files, duplicate definitions, language breakdown | examples/source-code.md |
complexity [expr] |
Functions ranked by cyclomatic complexity (worst-first) — maintenance hotspots. Also reports SonarSource cognitive complexity (nesting-weighted; Go + every tree-sitter language, Swift included since #491) | examples/source-code.md |
dead-code [expr] |
Candidate unreferenced definitions (functions / types nothing calls) — heuristic, for review | examples/source-code.md |
unused-exports [expr] |
Exported symbols referenced only within their own package — candidates to unexport | examples/source-code.md |
test-gaps [expr] |
Production files whose functions are never referenced from a test — no coverage run needed | examples/source-code.md |
coverage-gaps <profile> |
Functions below a coverage threshold from a Go coverage profile (go test -coverprofile) |
examples/source-code.md |
churn-owners [--expr] |
Ownership / bus-factor per directory from git authorship — single-maintainer hotspots | examples/source-code.md |
coupling [--top N] |
Afferent/efferent coupling + instability (Martin metrics) — fragile architectural seams. Go (packages) + Rust (crates) + Swift (SwiftPM target modules) + JVM Java/Kotlin/Scala (packages) + C# (namespaces) + Python (packages) + JS/TS (directory modules) + PHP (namespaces); more langs → #467 | examples/source-code.md |
circular |
Circular dependencies — strongly-connected components in the first-party import graph (Tarjan SCC). Same manifest-based language selection as coupling, so multi-language |
examples/source-code.md |
review [--base <ref>] |
Diff-scoped review gate — runs the per-file analyses (cyclomatic + cognitive complexity, dead-code) over the files changed in the git diff and emits a pass/warn/fail verdict; exit code gates CI / pre-commit | examples/source-code.md |
api-diff <tree-a> <tree-b> |
Removed exported symbols between two trees — a breaking-change release gate | examples/source-code.md |
playground [expr] -d <dir> |
Interactive TUI — type a CEL expression and watch a directory's files filter live as you type; prints the final expression on exit. Pass --embedding-model for semantic mode: a natural-language query box ranks files by similarity and the CEL box filters live |
examples/playground.md |
watch [expr] -d <dir> |
Continuously watch directories; emit each new / changed file that matches — the inverse of search |
examples/watch.md |
diff <tree-a> <tree-b> --op <set-op> |
Cross-tree set operations by sha256 — what's in A but not B, the intersection, content drift between same-named files | examples/diff.md |
organize <expr> --link-into <template> |
Build a templated symlink / copy tree from results — {raw_vendor}/{taken_at_year}/{basename} etc. |
examples/organize.md |
lines <path> --start --end |
Print a line range | examples/read-lines.md |
detect-project [dir] |
Identify project type(s) of a directory | examples/projects.md |
find-projects [root] |
Walk a tree listing every project subdirectory | examples/projects.md |
which-project <path> |
Walk UP from a file/dir to its nearest enclosing project root | examples/projects.md |
config-paths |
Print platform-specific project-type config paths | examples/projects.md |
hash-set build|info |
Compile a text / NSRL-CSV hash list into a bbolt hashset (or print its counts) for --hash-allowlist / --hash-denylist |
examples/hashsets.md |
embed list|pull|warm |
Manage Ollama embedding models for semantic search — list installed/recommended, pull a model, pre-warm a tree's embeddings cache | examples/semantic-search.md |
monitors |
List the dashboard URLs of every running instance (mcp / watch started with --monitor) |
examples/monitoring.md |
index-stats [--index-path] |
Inspect the on-disk attribute/body/embedding cache without running a search — entry counts, bytes used, hit/miss counters | examples/indexing.md |
mcp |
Run as a Model Context Protocol server | MCP server mode |
The source-aware code-intelligence commands (find-definition through api-diff) share a cross-file symbol + import graph and Go/tree-sitter symbol extraction; examples/source-code.md walks through the whole family. Each also has a matching MCP tool (see MCP server mode) — CLI and MCP are at near-total parity.
file-search-on --list prints the canonical schema (every attribute, every built-in function, every registered content type) — useful for "what can I filter on?" exploration.
Output formats
file-search-on '...' -o bare # paths only — pipes well into xargs / fzf
file-search-on '...' -o default # path \t [content-type] \t size
file-search-on '...' -o verbose # multi-line per match with every attribute
file-search-on '...' -o json # NDJSON, one match per line
file-search-on '...' --format '{{.Path}} ({{.WordCount}} words)'
The code-analysis commands complexity, dead-code, find-matches, coverage-gaps, unused-exports, duplicate-functions, and review also accept -o sarif — a SARIF 2.1.0 document that GitHub Code Scanning (and other CI) ingest, so findings show up as inline PR/code alerts:
file-search-on complexity 'is_source && language=="go"' -d . -o sarif > complexity.sarif
file-search-on dead-code -d . -o sarif > dead-code.sarif
file-search-on find-matches 'TODO|FIXME|HACK' -d . -o sarif > todos.sarif
file-search-on coverage-gaps cover.out -d . -o sarif > coverage-gaps.sarif
file-search-on unused-exports -d . -o sarif > unused-exports.sarif
file-search-on duplicate-functions -d . -o sarif > duplicate-functions.sarif
Content search
CEL's standard string methods (contains, startsWith, endsWith, matches) work on every string attribute. Pass --body to populate the body variable from text-based files (markdown, source, csv, json, xml, html, plus is_text) and filter on full file content:
file-search-on 'is_source && body.contains("panic")' --body -d ./internal
file-search-on 'is_source && body.matches("(?i)\\bTODO\\b")' --body
file-search-on '...' --sort word_count --order desc --limit 5
Top-K queries (--sort + --limit) buffer the full result set, sort, then truncate. Without --sort, --limit returns the first N in walk order.
For custom ranking — combining multiple attributes or semantic similarity into a single score — pass a CEL expression to --rank:
# Hybrid semantic + recency: weight similarity at 70%, fresh files at 30%
file-search-on 'is_pdf' \
--semantic-query "Q4 revenue forecast" \
--embedding-model nomic-embed-text \
--rank 'similarity * 0.7 + (mod_time > timestamp("2025-01-01T00:00:00Z") ? 0.3 : 0.0)' \
--limit 10
# Promote PDFs to the top of a mixed result set
file-search-on 'is_pdf || is_office || is_markdown' --rank 'is_pdf' --limit 20
The rank expression evaluates per file (after the filter). Higher values rank first; --order asc flips. See examples/ranking.md for the full cookbook.
Stats and reconnaissance
file-search-on stats -d ~/Downloads # by content_type (default)
file-search-on stats 'is_image' -d ~/Pictures --group-by camera_make
file-search-on stats 'is_source' -d ./src --group-by language
file-search-on stats 'is_image' -d ~/Pictures --group-by taken_at_year
file-search-on stats --dir ~/docs --dir ~/posts --group-by ext # multi-root aggregation
group_by keys: content_type (default), ext, dir, language, camera_make, camera_model, lens, artist, album, genre, kernel, binary_format, binary_type, frontmatter_format, plus time-bucket keys (mtime_year/month/day, taken_at_*, sent_at_*, date_*). Unrecognised keys silently fall back to content_type.
Project-type detection
file-search-on detect-project ~/my-app
file-search-on find-projects ~/Code --type go --type rust
file-search-on 'is_source && project_type == "go"' \
--resolve-projects --prune-build-artefacts -d ~/Code
file-search-on config-paths # where to drop user-wide / per-project YAML
--resolve-projects walks up from each file's directory to the nearest project root and sets project_type (string), project_types (list), and is_static_site (bool — fires for hugo / jekyll / eleventy / astro / gatsby / mkdocs / docusaurus / pelican). --prune-build-artefacts does a pre-walk to discover all project subdirectories under the search root and skips their canonical artefact directories (vendor, node_modules, target, __pycache__, .venv, bin, obj, .terraform, public, _site, dist, …). Custom project types are user-definable via CEL — drop a YAML at the path printed by config-paths. Full guide: examples/projects.md.
Duplicates and disk-eaters
file-search-on duplicates -d ~/Pictures # all duplicates under a tree
file-search-on duplicates 'is_image' -d ~/Pictures # scope to photos
file-search-on duplicates -d /Volumes/backup --min-size 1048576 # skip files < 1 MiB
file-search-on duplicates -d ~/Downloads -o json
Two-pass: files with unique sizes are skipped before any hashing. With --index-path, hashes are cached alongside (size, mtime) so repeat runs are free.
For SIMILAR (not identical) files — catching typo edits, regenerated headers, template copies that exact-hash dedup misses — use the SimHash-based near-duplicates subcommand:
file-search-on near-duplicates -d ~/notes # 0.85 similarity default
file-search-on near-duplicates 'is_markdown' -d ~/notes --threshold 0.95 # whitespace/typo only
file-search-on near-duplicates 'is_source && language == "go"' -d ./src --threshold 0.75
Fingerprints cache via --index-path alongside the exact hash; repeat runs skip body extraction AND SimHash compute. See examples/near-duplicates.md.
Common flags
-d <dir> (repeatable for multi-root walks), --exclude <glob> (basename, repeatable), --respect-gitignore, --timeout 30s (partial results returned on expiry), --workers N, --index-path <file.db> (override the per-cwd default index — see examples/indexing.md), --no-index (opt out of on-disk caching for hermetic runs).
Profiling
Root-level flags (work with any subcommand) capture a Go runtime profile of the run:
--cpuprofile <file> (CPU profile, open with go tool pprof <file>), --memprofile <file> (heap profile written after the command finishes), --trace <file> (execution trace, open with go tool trace <file>). For example, profile a large walk:
file-search-on search 'is_source' -d ./big-tree --cpuprofile cpu.prof
go tool pprof -top cpu.prof
For the long-running servers, mcp and watch accept --pprof, which mounts the live /debug/pprof/* endpoints on the monitoring dashboard (loopback-only, off by default) so you can profile a running server without restarting it:
file-search-on mcp --pprof --monitor-addr :9090
go tool pprof http://localhost:9090/debug/pprof/profile # 30s live CPU profile
curl -s http://localhost:9090/debug/pprof/heap # heap snapshot
Release binaries are built with symbols stripped (-s -w), so CPU profiles carry less symbol detail there; heap and goroutine profiles are unaffected.
.git is always pruned from every walk (CLI and MCP) — its objects/refs/logs are never searched or indexed. search and find-matches expose --include-git (CLI) / include_git (MCP) to walk into it; pointing -d directly at a .git directory also works (the walk root is exempt).
Pointing at a non-default Ollama
For semantic search and search_semantic (MCP), the embedding HTTP endpoint resolves in this order:
--embedding-server <url>flag (CLI ormcpsubcommand)$OLLAMA_HOSTenvironment variablehttp://localhost:11434(built-in default)
So a remote Ollama box on the LAN works without a per-invocation flag: export OLLAMA_HOST=http://gpu-box:11434. See examples/semantic-search.md for the full setup.
Recipes
Focused recipe collections live under [examples/](https://github.com/richardwooding/file-s
No comments yet
Be the first to share your take.