scry
grep for images. Find images by what they show, the text in them, or their metadata.

The first command ranks images by what they show, best first. The second finds text in images. The third keeps the photos from summer 2024 that have a GPS location. The fourth shows the matches in scatter mode, where you pick one by eye.
scry prints paths, so it works in pipelines. It runs on your CPU, and after a one-time model download it works offline.
Install
Linux and macOS, on x86_64 and aarch64.
# Prebuilt binary, installed to ~/.local/bin
|
# With cargo-binstall
# From source (Rust 1.94 or newer)
# With Nix, on Linux or Apple silicon
The crate is scry-cli, because scry is taken on crates.io. The binary is scry.
The first -q search downloads the SigLIP 2 model (about 500 MB), and -l needs only its image part (about 190 MB). The first -t or -o search downloads the OCR models (about 12 MB). scry checks each model against a pinned SHA-256 hash and keeps it in its cache directory: $XDG_CACHE_HOME/scry (usually ~/.cache/scry) on Linux, ~/Library/Caches/scry on macOS, or $SCRY_CACHE.
Examples
# A query in any language
# Bears, but not teddy bears
# Images that look like this one
# Summer 2024, a Fujifilm camera, 4000 px wide or more
# PNG images from the last 7 days
# The newest screenshot that shows an error
# The text that scry reads in an image, and each invoice number with its file
# The receipts, newest first: choose by meaning, then sort by date
|
# Pick images by eye and open them. On macOS, use open, not xdg-open.
|
# Read NUL-separated paths from another tool
| |
Options
scry [OPTIONS] [PATH]...
Each PATH is a file or a directory. With no PATH, scry reads image paths from stdin, one per line, if stdin is a pipe or a file. Otherwise it searches the current directory. An image must pass all the options that you give.
| Option | |
|---|---|
| Meaning | |
-q, --query <TEXT> |
Rank images by how well TEXT describes them, in any language. |
-l, --like <IMAGE> |
Rank images by how similar they look to IMAGE. IMAGE itself is not in the output. |
-x, --not <TEXT> |
Drop images that are closer to TEXT than to the query. Repeatable. Needs -q. |
--min <SCORE> |
Drop images that score below SCORE. Needs -q or -l. See scores. |
| Text in images | |
-t, --text <REGEX> |
Keep images whose text matches REGEX. Repeatable: all must match. |
-i, --ignore-case |
Ignore case in REGEX. Without -i, a REGEX ignores case only if it has no capital letter. |
-o, --show-text |
Print the text after each path and a tab. With -t, print only the parts that match, separated by tabs. |
| Metadata | |
--width, --height <RANGE> |
Image size in pixels. |
--date <RANGE> |
The date the photo was taken (EXIF). For an image without one, such as a screenshot, the time the file changed. |
--camera <REGEX> |
Camera make and model, for example 'fujifilm x-t4'. Ignores case. |
--gps, --no-gps |
Only images with, or without, a GPS location. |
| Files | |
-g, --glob <GLOB> |
Include paths that match GLOB, or exclude them with a leading !. A GLOB with a / matches the path below each PATH. Ignores case. Repeatable. Also filters paths from stdin. |
--hidden |
Search hidden files and directories. |
--no-ignore |
Do not read .gitignore and .ignore files. |
| Output | |
-s, --scatter |
Pick images by eye, one at a time. See scatter mode. |
--grid |
Pick images by eye from pages of thumbnails. Not with -s. See grid mode. |
--order <ORDER> |
Sort by --date: newest or oldest first. Not with -q or -l. |
-n, --limit <N> |
Print at most N paths, or all. Default: 20 with -q or -l and no --min, -s or --grid, else all. |
-S, --score |
Print the score and a tab before each path. Needs -q or -l. |
-p, --preview |
Show a thumbnail above each path when stdout is a terminal. |
-0, --null |
Separate paths with NUL, on stdin and on stdout. |
A RANGE is A..B, A.., ..B or A. Both ends are inclusive.
- Pixels are integers.
- A date is
YYYY,YYYY-MMorYYYY-MM-DDand covers the full period. So--date 2024-06..2024-08ends with August 31. - An age is
Nh,Nd,Nw,NmoorNyback from now, and needs... So--date 7d..is the last 7 days, and..1yis older than one year. - All times are local. To keep only camera photos, add
--camera ..
scry uses one order for each search. With -q or -l, the best match comes first. With --order, the newest or oldest image comes first. Else, scry prints each match when it finds it, and paths from stdin keep their order. A sorted search must see all images before it prints the first path. Then -n cuts the slow steps short: scry reads the text in the images in that order and stops when it has enough matches.
Exit status: 0 scry printed a path, 1 nothing matched, 2 an error (also when nothing matched and some files could not be read), 130 you quit scatter or grid mode.
Pick by eye
A description can match many images. scry has two modes to pick the right one by eye. Scatter mode (-s) shows the matches one at a time, so your eye checks only the newest image. Grid mode (--grid) shows them in pages of up to 26, so you can compare them side by side.
Like fzf, both modes draw on the terminal and write only the result to stdout. So scry -s ... | xargs ... and $(scry --grid ...) work.
Scatter mode
scry -s shows the images one at a time, each at a different place on the screen. A new image lands on top and covers the part of the screen that has been visible longest. No image disappears while you can see a part of it, so your eye goes only to the newest one.
With -q or -l, scry scores all images before it shows the first one. Then the best matches come first and are bigger. With --order, the images come in date order. Else, the images come in random order with no model download, so scatter mode starts at once and works well to browse a folder.
| Key | Action |
|---|---|
Space |
Pause or play. |
←, → |
Go back or forward one image, and pause. Hold ← to rewind. → at the newest image shows the next one. |
a to z |
While paused, select the image with that hint letter. |
A to Z |
Show that image full screen. There, ← and → go through the images, Enter prints it with the marked images, Tab marks it, and any other key goes back. |
Tab |
Mark or unmark the selected image. |
Enter |
Print the marked images in mark order, or else the selected image, and quit. With no selection, select the best image in view. |
+, - |
Show images faster or slower. |
], [ |
Make images larger or smaller. |
Esc, Ctrl-C |
Quit and print nothing. |
When you go back, the newest image on screen is selected and gets the hint letter a. So if you see a good image too late, press ← until it is selected, then press Enter, or A to see it full screen. Space plays on from there.
Grid mode
scry --grid shows the images in pages of thumbnails. A page holds up to 26 images, one for each hint letter. The terminal size sets the number of columns and rows. All tiles have the same size and touch each other. scry crops each image to fill its tile, and A to Z show the whole image.
The images keep the order of the search, from left to right and top to bottom. So with -q or -l, the best match is a on the first page. A sorted search must see all images before it shows the first page.
| Key | Action |
|---|---|
a to z |
Select the image with that hint letter. |
A to Z |
Select that image and show it full screen. There, ← and → go through the images, also to other pages, Enter prints it with the marked images, Tab marks it, and any other key goes back. |
Tab |
Mark or unmark the selected image. |
Enter |
Print the marked images in mark order, or else the selected image, and quit. |
→, Space, PageDown |
Show the next page. |
←, PageUp |
Show the previous page. |
Esc, Ctrl-C |
Quit and print nothing. |
When the grid opens, and after each page change, the first image of the page is selected. The status line shows the name of the selected file and the page, for example page 2/5.
Terminals
kitty, Ghostty, iTerm2, WezTerm, foot and other terminals with sixel support show full-resolution images. Other terminals get Unicode half blocks, at low resolution. kitty and Ghostty read the images from shared memory when scry runs on the same computer, which keeps both modes fast, also in tmux. In tmux, scry turns on allow-passthrough for its own pane.
In both modes, the selected image has a yellow edge and a white hint letter. A marked image has a green edge and a check mark. In kitty and Ghostty, the edge colors follow the terminal palette. With half blocks, the edge is a ring of half blocks. With sixel and iTerm2 images, the edge stays gray, so the white hint letter and the status line show the selection.
How it works
scry runs the fast filters first, and the slow ones only on the images that are left: path filters, then the image header and EXIF, then SigLIP 2 embeddings, then OCR. Inference runs on rten and OCR on ocrs, both in Rust. scry needs no Python, no ONNX Runtime and no GPU.
scry has no vector database. It compares the query with each candidate directly, which is exact and works with all filters.
The cache, cache.redb in the cache directory, keeps the embeddings, OCR text and metadata of each file until the file changes. The first search over a folder is slow: on an 8-core laptop CPU (Ryzen 9 8945HS), scry embeds or reads the text of about 8 to 10 images per second. Later searches process only new or changed images, and take well under a second.
Scores
-q: the probability (0 to 1) that the text describes the image. It does not depend on the other images, so a--minvalue means the same in every folder.-l: the cosine similarity of the two images, from -1 to 1.-Sprints 3 significant digits, with an exponent below 0.0001:2.5e-6is 0.0000025.
The probabilities are small. A correct image for an English description usually scores above 0.01, and an unrelated image below 0.0001. Queries in other languages score lower. To choose --min, run a search with -S once and look at the scores of your images.
FAQ
Which formats does scry read? JPEG, PNG, GIF, WebP, BMP and TIFF. On macOS also HEIC, AVIF and JPEG XL, through the image decoder of macOS. Not camera RAW yet, and not HEIC, AVIF or JPEG XL on Linux. scry counts the files that it skips and tells you at the end. It also skips images too large to decode safely (more than 256 MiB of pixels).
Why does -t not find a text that I can see?
Use -o to see the text that scry reads. OCR can read words in an order that you do not expect, or read a letter wrong. A meme with "ROAD TRIP" on two lines can read as O ROAD IH TRIP, so -t 'road.*trip' finds it and -t 'road trip' does not. OCR reads only Latin script for now.
Why are some images missing?
By default, scry skips hidden files and the paths in .gitignore. Use --hidden and --no-ignore to include them. scry also skips the Photos library of macOS, which holds several copies of each photo. To search it, give its path. scry follows symbolic links to images, but not to directories.
How do I clear the cache, or use scry without internet?
Delete cache.redb in the cache directory to clear the cache. To use scry on a computer without internet, copy the models directory from the cache directory of a computer where scry downloaded the models.
Support
If scry saves you time, you can buy me a coffee.
Credits
SigLIP 2 is by Google, under Apache 2.0, and the ONNX export is from onnx-community. The OCR models and the inference runtime are from Robert Knight's ocrs and rten. ratatui-image draws the images in the terminal. The photos in the demo are from Unsplash, through Lorem Picsum.
License
Licensed under either of Apache License 2.0 or MIT license, at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.