Skip to main content

nfind

CI PyPI Python Downloads License Docs Buy Me a Coffee

Find files by describing them in natural language.

The name is short for natural-findfind, but driven by a natural-language description instead of a filter expression. nfind takes a plain text description, asks an LLM to write a small filter function for it — in Python (filter_paths) or Node.js (filterPaths) — and runs that function against your file tree to print the matching paths — a natural-language cousin of find that can answer deep structural questions about your files.

Like find, nfind walks directory trees recursively. Unlike find, it skips common VCS, dependency, virtual-environment, and cache names by default (including .git, node_modules, .venv, and __pycache__). Pass --no-ignore to walk the complete tree, or --max-depth N to limit recursion.

The generated code is never executed on your machine directly. By default it runs inside a disposable, hardened Docker container with the search directory bind-mounted read-only, networking disabled, all Linux capabilities dropped, and CPU, memory, and process limits applied. Experimental alternate backends — Apple Containers (--sandbox apple), Podman (--sandbox podman), and nerdctl/containerd (--sandbox nerdctl) — are available with weaker or experimental guarantees; see the Safety model below.

Demo

A recorded demo session — click the image to play it on Asciinema:

asciicast

Why nfind?

nfind sits in a gap not filled by other file-search tools. It combines three things at once:

  1. Natural language — you describe what you want, not a query grammar or a find incantation.
  2. A standalone, auditable filter program, not a one-liner — the LLM writes an actual Python/Node filter program that you can review, save, and run directly. Because it is a real, complete program, it can express deep structural, relational, and computed questions (e.g. "directories that contain only audio files", "Python files and their line counts") that a glob or a single find predicate can't.
  3. Local, disposable, hardened sandbox execution — the program runs over your real tree inside a disposable, hardened sandbox (with your directory mounted read-only), so it can open and inspect files — yet your file list and contents never leave the machine (only your prompt is sent to the model). The default Docker backend disables networking. Apple Containers do it on macOS 26+.

Each neighbouring category has only part of this:

Tool category Natural language Reads contents / structure Runs locally
find / fd / ripgrep, Spotlight (mdfind) partial
fselect / osquery (SQL over files)
NL→command helpers (sgpt, gh copilot) ✗ (just a one-liner)
Send-the-file-list-to-an-LLM tools (e.g. lfind) ✗ (filenames only)
nfind

Additionally, while system-level search tools like Spotlight and Siri only work locally on physical machines, nfind is a headless CLI tool that works perfectly over remote terminals via SSH, running the same sandboxed searches on your remote servers.

In one line: nfind is like asking an analyst to write and run a one-off script against a folder — safely, and without your files leaving your machine. See docs/comparison.md for the full breakdown.

Requirements

  • Python 3.11+
  • Docker installed and running, or an experimental alternate backend — Apple Containers on macOS via --sandbox apple, Podman via --sandbox podman, or nerdctl/containerd via --sandbox nerdctl (see Safety model)
  • An API key for your provider — OPENAI_API_KEY by default, or the matching key for another provider

Install

uv tool install nfind
# or
pip install nfind

To install from a local checkout:

uv tool install .
# or
pip install .

Usage

export OPENAI_API_KEY=sk-...

# Search the current directory
nfind "directories that contain only audio files"

# Search a specific directory
nfind "Python files that import requests" ./src

# Include normally ignored paths such as .git, node_modules, and .venv
nfind "Python files" ./src --no-ignore

# Search specific files (a root may be a file, not just a directory)
nfind "files that define a class" ./src/app.py ./src/models.py

# Help (both forms work)
nfind -h
nfind --help

# Version
nfind --version   # or -V

Output modes

By default nfind prints one path per line, like find. When your prompt asks for extra per-file information, the generated filter attaches it to each result and you can surface it:

# Default: clean, pipeable list of paths
nfind "Python files that import os"

# Verbose: path plus any extra fields the prompt produced
nfind "Python files, and for each the number of lines" --fields
# /path/to/a.py	lines=42

# JSON: machine-readable records (path plus extra fields) with a count
nfind "Python files, and for each the number of lines" --json
# { "count": 2, "results": [ { "path": "...", "lines": 42 }, ... ] }

# Extract: items inside files — one match per line (path[:line]<TAB>payload)
nfind --extract "every TODO comment, with its file and line number" ./src
# /src/app.py:42	handle retry
nfind --extract "every TODO with file and line" ./src | wc -l   # counts matches

Under the hood nfind does essentially one thing — select a subset of your files, each optionally annotated with the extra fields your prompt asked for — and every mode above just renders those records. --extract is the one step that reaches inside files (TODOs, URLs, fields) rather than listing whole ones; counting, answering, or editing is left to the pipe you already use (| wc -l, | jq, | xargs).

--json and --fields are mutually exclusive (as are --extract and --fields). The richer output appears only when the prompt asks for it; otherwise every mode just lists paths. See docs/output-modes.md.

Runtimes

The model picks the runtime per prompt — Python (default) or Node.js, when the JS/TS ecosystem fits better (e.g. parsing TypeScript with ts-morph). nfind runs the filter in the matching sandbox image; both run under the same isolation. See docs/runtimes.md.

nfind "TypeScript files that export a default, using ts-morph" ./src

Dependencies

Some prompts need a library (reading MP3 tags, image sizes, PDF text). The generated filter declares the packages it imports — pip for Python, npm for Node — and nfind installs them into a derived sandbox image, but only approved packages. A built-in default list (per runtime) installs without asking; new packages are confirmed and then remembered.

nfind "MP3 files whose title tag contains 'live', using mutagen" ~/Music   # prompts if new
nfind "images larger than 4000px on a side" ~/Photos --yes                 # approve without asking
nfind "files containing TODO" . --no-deps                                  # standard library only

The Python defaults include tree-sitter and per-language grammar wheels (tree-sitter-python, -go, -rust, …), so a filter can parse source structure — functions, imports, classes — without a dedicated runtime (the Node.js runtime is reserved for type-aware TS/JS analysis). Packages are installed at image-build time (which needs network); the default Docker container that runs the filter has no network. See docs/dependencies.md.

macOS metadata

On macOS, --macos-meta exposes a small slice of macOS-specific metadata — Finder tags and download provenance (the quarantine flag and "where from" URLs) — to the filter. These live on the host and aren't visible inside the Linux sandbox, so nfind reads them host-side (read-only) and passes them in. This unlocks queries that combine macOS metadata with file contents — something neither Spotlight nor a container-only filter can do alone:

nfind "PDFs I downloaded from the web that mention 'invoice', using pypdf" ~/Downloads --macos-meta
nfind "files tagged Red whose contents contain a TODO" ~/Projects --macos-meta

For pure-metadata lookups ("everything tagged Red"), Spotlight (mdfind) is faster. The flag is a no-op off macOS. See docs/macos-metadata.md.

Reviewing the generated code

The filter is generated by an LLM, so you may want to see it before it runs:

# Print the generated filter (to stderr) before running it
nfind "files with no extension" --show-code

# Save the generated filter to a file
nfind "files with no extension" --save filter.py

# Replay a saved filter through the sandbox (no LLM call; Docker also has no network)
nfind --run filter.py
nfind --run filter.py ./other-directory   # different search root

# Show the code and ask for confirmation before running (aborts on "no")
nfind "files with no extension" -i        # or --confirm

The code is printed to stderr, so stdout stays a clean, pipeable list of paths even with --show-code. On a terminal it is syntax-highlighted with Pygments; the highlighting is disabled when NO_COLOR is set or when stderr is redirected.

If the model's reply doesn't validate (malformed JSON, wrong function shape, an invalid package name), nfind feeds the error back and retries a few times before giving up; retry notices are printed to stderr.

The first run builds the worker image for the chosen runtime (nfind-search-paths:latest for Python, nfind-search-node:latest for Node.js); later runs reuse it. Pass --rebuild to force a fresh build.

Useful options

The options newcomers reach for most often:

Option Default Purpose
--model openai/gpt-5.4 Model used to generate the filter; provider/model for non-OpenAI (see Providers)
--json off Output records (path + extra fields) as JSON
--fields / -f off Show extra per-path fields alongside each path
--show-code off Print the generated filter before running
--save / --run Save the generated filter, or replay a saved one without an LLM call
--sandbox docker Sandbox backend: docker, experimental apple on macOS, experimental podman, or experimental nerdctl (containerd)
--no-ignore off Include default ignored directories such as .git and node_modules
--max-depth N unlimited Descend at most N levels below each search path
--yes / --no-deps off Approve requested packages without prompting, or reject third-party packages entirely

Run nfind -h for the authoritative list, or see the full CLI reference for every option (resource limits, output bounds, --exclude, --extract, --print0, --macos-meta, --confirm, and more).

Providers

By default nfind uses OpenAI. To use another provider, pass --model provider/model; nfind reuses the OpenAI SDK against that provider's OpenAI-compatible endpoint, so there is no extra dependency to install — just set the provider's API key.

nfind "files with no extension"                          # OpenAI (OPENAI_API_KEY)
nfind "..." --model anthropic/claude-sonnet-4-6          # ANTHROPIC_API_KEY
nfind "..." --model gemini/gemini-2.5-flash              # GEMINI_API_KEY
nfind "..." --model groq/llama-3.3-70b-versatile         # GROQ_API_KEY
nfind "..." --model openrouter/<vendor>/<model>          # OPENROUTER_API_KEY (near-universal)
nfind "..." --model ollama/llama3.1                      # local, no key

Supported prefixes: openai, anthropic, gemini, groq, mistral, deepseek, xai, openrouter, ollama, lmstudio. Each reads its own *_API_KEY (local servers need none). nfind handles providers without strict JSON mode automatically.

Prefer a capable model — it's the cheapest place to spend quality. The model does just one thing: turn your prompt into the filter program, and the correctness of the whole search rides on that code. But the call is tiny — a short prompt in, a small filter out (your file list and contents are never sent) — so even a top-tier model usually costs a fraction of a cent per query. And the result is reusable: --save the filter once and --run it forever with no further LLM calls. Pay once for a strong model to write good, reusable code; weaker ones may only save a fraction of a cent while needing retries or producing a subtly wrong filter.

What can you ask?

The prompt is free-form. What makes nfind different is the kind of question it can answer — ones that read contents or structure, compute a value, or relate files across a tree, and so are out of reach for find and grep:

# Cross-file relationships — the answer depends on two files, not one
nfind "directories that contain both RAW and JPEG files with the same stems (already converted)" ~/Photos
nfind "client directories with a signed contract PDF but no invoice of matching stem (delivered work never billed)" ~/Clients

# Structural correctness a linter would catch — but across any tree, described in a sentence
nfind "Kubernetes Deployment manifests with no resource limits set" ~/k8s
nfind "Jupyter notebooks that contain cells with error or traceback outputs" ~/notebooks
nfind "PDF files that still contain selectable text under a black redaction rectangle (failed redaction), using pypdf" ~/discovery

# Binary & metadata introspection
nfind "JPEG files shot with an ISO above 6400 (likely noisy), using pillow" ~/Photos
nfind "GPX files, with their total elevation gain in meters, using gpxpy" ~/tracks --json

# macOS provenance × file contents (Spotlight can do each alone; nfind combines them)
nfind "PDFs I downloaded from arxiv.org that mention 'interpretability', using pypdf" ~/Papers --macos-meta

For dozens more, grouped by profession — web dev, data & AI, authors, photographers, DevOps, freelancers — see the Cookbook.

Library use

from nfind import search

# Returns a list of records, each a dict with at least a "path" key (a host path).
# When the prompt asks for extra per-file values, they appear as additional keys.
records = search(".", "directories that contain only audio files")
paths = [record["path"] for record in records]

Safety model

To minimize the blast radius of running LLM-generated code locally, nfind uses a sandboxed execution model that provides strong isolation guarantees when running on Docker:

  • Search roots are mounted read-only under /data; results are mapped back to host paths afterward.
  • The default Docker backend runs with --network none, --cap-drop ALL, --security-opt no-new-privileges, a read-only root filesystem, and a small tmpfs for scratch space.
  • --sandbox apple uses Apple Containers. It keeps read-only mounts/root, dropped capabilities, CPU/memory limits, and a tmpfs. On macOS 26+ nfind uses --network none; on macOS 15 Apple does not support that flag, so nfind falls back to --no-dns and raw IP network access may still be possible. Apple --cpus values must be whole numbers, so fractional CPU limits are rejected.
  • --sandbox podman uses Podman with the same hardened run command as Docker (--network none, dropped capabilities, no-new-privileges, read-only root, and pids/memory/CPU/tmpfs limits). On rootless Podman it also remaps the read-only mount to the worker user (--userns=keep-id) so the non-root worker can read it. It is experimental because it has been validated only on limited hosts and rootless isolation differs from a rootful Docker daemon, so nfind prints a warning before running.
  • --sandbox nerdctl runs the worker on containerd via the nerdctl CLI (e.g. Lima or Rancher Desktop), using the same hardened run command as Docker, including --network none. It is experimental: validated on Linux CI against rootful containerd, but on rootless nerdctl the mount may be unreadable by the non-root worker (no keep-id remap like Podman's), so prefer rootful containerd; nfind prints a warning before running.
  • The host validates that the filter returns only paths it was given, so generated code cannot inject arbitrary paths into the output.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nfind-0.2.1.tar.gz (61.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nfind-0.2.1-py3-none-any.whl (78.4 kB view details)

Uploaded Python 3

File details

Details for the file nfind-0.2.1.tar.gz.

File metadata

  • Download URL: nfind-0.2.1.tar.gz
  • Upload date:
  • Size: 61.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for nfind-0.2.1.tar.gz
Algorithm Hash digest
SHA256 23498e283d0b0f1aaa75e3267cf18eaf3fc3c0b3e227fd7e95280370db3e82e6
MD5 ac7bc355a30de8d3073a184d623c5961
BLAKE2b-256 6bb84d3aae50046f63c9782e7679f0282e00b5359f3f292fe2c66c0a0ff51212

See more details on using hashes here.

Provenance

The following attestation bundles were made for nfind-0.2.1.tar.gz:

Publisher: publish.yml on deeplook/nfind

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nfind-0.2.1-py3-none-any.whl.

File metadata

  • Download URL: nfind-0.2.1-py3-none-any.whl
  • Upload date:
  • Size: 78.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for nfind-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 5297171ad87557ee6cf3c6f4d50ac48f472a3c29346b1dc1154e2f0dac23caa3
MD5 3fe060aaa0714f59aad176279070e6d9
BLAKE2b-256 afbaca0dfe89f97b6b9a8544f863b40e318c5598cf078420078517d125292919

See more details on using hashes here.

Provenance

The following attestation bundles were made for nfind-0.2.1-py3-none-any.whl:

Publisher: publish.yml on deeplook/nfind

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page