> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloudstic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# cloudstic find

> Locate a file across every snapshot in the repository

Locate a file across every snapshot in the repository, without knowing which snapshot holds it.

Every other read command takes a snapshot as input. `find` returns one as output, which is what makes it useful for the case you actually hit: recovering a file that is no longer in the latest snapshot. If you know the file is in the latest snapshot, [`cloudstic ls`](/commands/ls) already finds it.

## Usage

```bash theme={null}
cloudstic find [pattern] [options]
```

<CodeGroup>
  ```bash By name theme={null}
  cloudstic find "vault.kdbx"
  ```

  ```bash By path theme={null}
  cloudstic find "Documents/**/report.pdf"
  ```

  ```bash By file identity theme={null}
  cloudstic find -id <file-id>
  ```
</CodeGroup>

`find` searches **every** snapshot by default. That is affordable because snapshots share structure: within a source lineage, Cloudstic walks the oldest snapshot once and then diffs consecutive ones, so an unchanged directory costs a single pointer comparison rather than another full walk.

## Patterns

The positional pattern is shorthand, and its shape decides what it matches:

* **No `/`** — matches the file's **basename**. `"vault.kdbx"` is the same as `-name "vault.kdbx"`.
* **Contains `/`** — matches the full **path**, relative to the source root. `"Documents/report.pdf"` is the same as `-path "Documents/report.pdf"`.

That split is not cosmetic. A name is already on the file's metadata object, while a path has to be reconstructed by walking up the parent chain, so a name query is meaningfully cheaper.

Glob syntax is the standard one per path segment (`*`, `?`, `[...]`), plus `**` for "zero or more path segments":

```bash theme={null}
# Any PDF, anywhere
cloudstic find "*.pdf"

# A PDF directly in Documents
cloudstic find "Documents/*.pdf"

# A PDF anywhere under Documents, at any depth
cloudstic find "Documents/**/*.pdf"
```

<Note>
  `*` never crosses a `/`. Use `**` when you mean "at any depth".
</Note>

## Matching options

All options given must match. There is no `or`; run two queries and combine the results yourself.

<ParamField path="pattern" type="string">
  Positional pattern. Matches the basename when it contains no `/`, the full path when it does.
</ParamField>

<ParamField path="-name" type="string">
  Match the file's basename against a glob. The explicit form of a `/`-free positional pattern.
</ParamField>

<ParamField path="-path" type="string">
  Match the file's full path against a glob. `**` spans any number of directories.
</ParamField>

<ParamField path="-regex" type="string">
  Match the file's full path against an RE2 regular expression. Unanchored, like `grep`.
</ParamField>

<ParamField path="-i" type="boolean" default="false">
  Make `-name`, `-path`, and `-regex` case-insensitive. This affects matching only; it says nothing about the case sensitivity of the filesystem the files came from.
</ParamField>

<ParamField path="-id" type="string">
  Match one source file ID. The ID is stable across renames and moves within a source, so this is the "show me this file's whole history" query.
</ParamField>

<ParamField path="-content-hash" type="string">
  Match files whose content has this SHA-256. Answers "where else do I store these exact bytes?"
</ParamField>

<ParamField path="-ref" type="string">
  Match one exact metadata object, as `filemeta/<hash>` or a bare hash.
</ParamField>

<ParamField path="-type" type="string">
  Match only files (`f`) or only folders (`d`).
</ParamField>

<ParamField path="-size" type="string">
  Match by size, using `find(1)` syntax: `+10M` (at least), `-1k` (at most), `4096` (exactly). Suffixes are binary: `k`, `M`, `G`, `T`.
</ParamField>

<ParamField path="-newer" type="string">
  Match files modified after a time.
</ParamField>

<ParamField path="-older" type="string">
  Match files modified before a time.
</ParamField>

<Tip>
  `-id`, `-content-hash`, and `-ref` compare against values the repository stores verbatim, so they are the cheapest queries available and the ones to reach for when you already know what you are looking for.
</Tip>

Times accept RFC3339 (`2026-01-31T09:00:00Z`), a plain date (`2026-01-31`), or a duration back from now (`7d`, `12h`, `30m`).

## Choosing which snapshots to search

<ParamField path="-snapshot" type="string">
  Search only this snapshot. Repeatable, and accepts a full hash, a unique prefix, or `latest`.
</ParamField>

<ParamField path="-source" type="string">
  Search only snapshots of this source URI, for example `local:./Documents`.
</ParamField>

<ParamField path="-tag" type="string">
  Search only snapshots carrying this tag. Repeatable.
</ParamField>

<ParamField path="-latest" type="integer">
  Search only the N newest selected snapshots.
</ParamField>

<ParamField path="-since" type="string">
  Search only snapshots created at or after a time.
</ParamField>

<ParamField path="-until" type="string">
  Search only snapshots created at or before a time.
</ParamField>

<Warning>
  `-since` and `-until` select **snapshots** by when the backup ran. `-newer` and `-older` select **files** by their modification time. The two are easy to confuse and do genuinely different things.
</Warning>

## Output options

<ParamField path="-by-content" type="boolean" default="false">
  Group results by content hash instead of by file. Changes grouping only, never which entries matched.
</ParamField>

<ParamField path="-max-results" type="integer" default="1000">
  Stop accumulating after N matching files. Scanning continues to completion, so the reported counters stay accurate.
</ParamField>

<ParamField path="-json" type="boolean" default="false">
  Write the full result to stdout as JSON.
</ParamField>

<ParamField path="-no-delta" type="boolean" default="false">
  Walk every snapshot in full instead of diffing consecutive ones. Much slower; it exists so the fast path can be checked against a simple one.
</ParamField>

## Reading the results

```plaintext theme={null}
$ cloudstic find "*.kdbx"

Documents/vault.kdbx  (3 versions)
  v1 filemeta/9f86d081  4.2 MiB  2026-07-21 09:14  7 snapshots 4e5d5487..410b18a2
  v2 filemeta/2c624232  4.1 MiB  2026-06-30 18:02  13 snapshots 1b645389..7d793037
  v3 filemeta/e3b0c442  3.9 MiB  2026-05-02 11:47  8 snapshots 5f9c3a11..a71e04bd

1 file, 3 versions across 28 snapshots (searched 31 snapshots in 1.8s)

Restore the newest version of Documents/vault.kdbx:
  cloudstic restore -path Documents/vault.kdbx 410b18a2 -output ./restored
```

A result is a **file**, carrying its **versions**, each carrying the **snapshots** it appears in. The trailing hint spells out the restore, because running `find` almost always means `restore` comes next.

### Why you do not get thousands of rows

A repository holds the same file many times over, and each kind of repetition is collapsed differently.

<AccordionGroup>
  <Accordion title="Backed up unchanged, night after night">
    A file backed up nightly and never edited has one metadata object shared by all thirty snapshots. It is reported as **one version credited with thirty snapshots**, not thirty identical rows.

    This is not a display convenience. The underlying references are literally equal, so one row is the accurate reading of what the repository contains.
  </Accordion>

  <Accordion title="Edited over time">
    Each edit produces a new version. You get one match with several versions, newest first, each with its own snapshot range.

    This is the view you want when recovering a file: "which version do I want?" is the real question, and a version with a date range is the answer to it.
  </Accordion>

  <Accordion title="Two files with identical content">
    `~/Documents/report.pdf` and `~/Backup/report.pdf` may hold identical bytes, but they are **different files** and stay separate matches. Collapsing them would be wrong: restoring one is not restoring the other.

    When finding duplicates *is* the goal, `-by-content` regroups the same matches by content hash.
  </Accordion>

  <Accordion title="One file in two places at once">
    A Google Drive file can live in two folders simultaneously, and hard links raise the same question. Every path is listed on its own line; none is silently picked.
  </Accordion>
</AccordionGroup>

### Renames

Results group by file ID rather than by name, and the name lives on the version. A renamed file is therefore **one match whose versions carry different names** — a rename history, as a consequence of how grouping works rather than a separate feature.

One consequence is worth stating plainly: a *name* query matches only the versions bearing that name.

```bash theme={null}
# Matches the versions named old-name.txt, and only those
cloudstic find "old-name.txt"

# Matches every version of that file, whatever it was called
cloudstic find -id <file-id>
```

Pulling in the post-rename versions would make the result set depend on grouping, which is how a search tool becomes unpredictable.

## Examples

<CodeGroup>
  ```bash Large files added recently theme={null}
  cloudstic find "*" -size +100M -newer 30d
  ```

  ```bash Find deleted key files theme={null}
  cloudstic find "*.key" -type f
  ```

  ```bash Duplicate content theme={null}
  cloudstic find "*.jpg" -by-content
  ```

  ```bash Narrow to one source theme={null}
  cloudstic find "invoice*.pdf" -source local:./Documents
  ```

  ```bash Narrow to a date range theme={null}
  cloudstic find "*.log" -since 2026-01-01 -until 2026-06-30
  ```

  ```bash Every copy of exact bytes theme={null}
  cloudstic find -content-hash 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
  ```
</CodeGroup>

## Machine-readable output

`-json` emits the full result. The schema is the same whether or not `-by-content` is set; only the grouping differs.

```bash theme={null}
cloudstic find "*.kdbx" -json | jq '.matches[].versions[0].snapshots[0].ref'
```

Finding nothing exits `0`, the same as `cloudstic list` on an empty repository and the same as `find(1)`. Scripts tell the cases apart by reading `matches`:

```bash theme={null}
if [ "$(cloudstic find "$NAME" -json | jq '.matches | length')" -eq 0 ]; then
  echo "not in any snapshot"
fi
```

Warnings about how the search ran are written to stderr, so they never contaminate a `-json` pipeline. They also appear in the JSON payload as a `warnings` field.

## Performance

The cost of searching a whole source lineage is roughly **one full walk plus the repository's actual churn** — not one walk per snapshot.

Within a lineage, Cloudstic walks the oldest selected snapshot in full and then diffs consecutive snapshots. Because the metadata tree is persistent, an unchanged subtree keeps its reference across snapshots, so the diff descends only where something actually changed. Metadata objects are content-addressed and immutable, so one already evaluated is never fetched or evaluated again — including across sources, which is what makes an identical file backed up from two machines cost one evaluation rather than two.

Two things to know about query cost:

* **A basename pattern is cheaper than a path pattern.** Paths are reconstructed rather than stored. A pattern like `Documents/**/*.pdf` is prefiltered on `*.pdf` first, so paths are resolved only for the PDFs.
* **`-regex` and patterns ending in `**` have no such prefilter**, so every entry's path must be reconstructed. `find` says so on stderr rather than appearing to hang.

<Tip>
  Use `-verbose` on a large repository to watch metadata objects being read.
</Tip>

## Limitations

`find` matches metadata, not content. There is no full-text search: matching inside file contents would mean fetching, decrypting, and decompressing every chunk in the repository, which is a different feature with a different cost model.

There is also no repository-side search index, by design. A stale or partially written index that silently returns no matches is a data-loss-shaped bug wearing a search feature's clothing, and the delta scan gets acceptable performance without one.

## Encryption

`find` requires an unlocked repository. File metadata is encrypted at rest, so there is no metadata-only access path, and no new exposure: anyone who can run `find` can already run `ls` and `restore`.

```bash theme={null}
cloudstic find "*.kdbx" -password "my secret passphrase"
```

<Note>
  Filenames are sensitive even when contents are not. `find` keeps matched paths out of default log output.
</Note>

## See Also

* [cloudstic ls](/commands/ls): List the file tree of one snapshot
* [cloudstic restore](/commands/restore): Restore files from a snapshot
* [cloudstic list](/commands/list): List all snapshots
* [cloudstic diff](/commands/diff): Compare two snapshots
