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 /mcp inside 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 adds live_photo_video_path, live_photo_video_size, live_photo_image_path
    Audio 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 .zpln cell_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 .appimage package_format, package_name, package_version, package_release, package_arch, package_kind, appimage_version
    VM bytecode Java .class (JVM), Python .pyc / .pyo, WebAssembly .wasm bytecode_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, .webloc 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
    Browser bookmarks Chromium-family (Chrome / Brave / Edge / Chromium / Opera / Vivaldi / Arc) Bookmarks JSON + Safari Bookmarks.plist bookmark_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) — predicates is_3d_model, is_stl, is_obj, is_gltf model3d_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.json is both is_node_manifest and is_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-OS is_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. Surfaces is_plist plus 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-based plist_kind registry 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 for com.example.bundle" in one query.

  • Browser bookmarks — Chromium-family Bookmarks (Chrome / Brave / Edge / Chromium / Opera / Vivaldi / Arc) and Safari Bookmarks.plist. Surfaces is_bookmark_file / is_chromium_bookmarks / is_safari_bookmarks plus bookmark_count, bookmark_folder_count, bookmark_folders, bookmark_urls, bookmark_titles, browser_vendor (chrome / chromium / edge / brave / opera / vivaldi / arc / safari), and bookmark_profile. With --body, the body CEL variable carries title\turl lines so body.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 --json output. All three are plain .json files with arbitrary names, so they're detected by a streaming top-level-JSON-shape discriminator rather than by extension. Surfaces is_chat_export plus per-format is_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, and chat_end_at. With --body, the body CEL variable carries one {timestamp}\t{author}\t{text} line per message so is_chat_export && body.contains("kubernetes") greps the conversation text across an entire export. See examples/chat-exports.md.

  • Screenshot OCR--ocr runs OCR over image/* files via the registered provider (macOS Vision today; Linux Tesseract / Windows.Media.Ocr deferred under the same hook). The recognized text populates the body CEL variable so body.contains("kubernetes") queries work over ~/Desktop screenshots 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 dev make ocr-helper builds it. On platforms without a registered provider, --ocr is 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_axes all populate for .woff2 collections 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 the name table (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), and fvar (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 detectiondetect-project / find-projects / which-project subcommands 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-level project_type filter) and --prune-build-artefacts (skip vendor/node_modules/target/__pycache__/public/_site etc. automatically). The is_static_site CEL 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 generic frontmatter map. 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, and point_in_polygon (for GPS bboxes / city outlines) let you write typo-tolerant and "sounds-like" queries against any string attribute. EXIF camera make in Nikkon instead of Nikon? Artist tag mistyped as Radiohad? Same query catches all of them. See examples/fuzzy-search.md.

  • Multiple output formatsbare (paths only), default, verbose (multi-line), json (NDJSON), or a Go text/template via --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:

  1. --embedding-server <url> flag (CLI or mcp subcommand)
  2. $OLLAMA_HOST environment variable
  3. http://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