Skip to content

Inputs and detection

The analyzer reads whatever a responder has: an archive from either collector, a collection already extracted, or a loose tree such as a copied home directory or a mounted disk image. A collection carries a manifest, so every file's host, user, home and agent come from it. A loose tree carries nothing, so the analyzer finds homes and agents with the collectors' own catalog and infers the user from the path, and its output says that it did. A file or directory it cannot read is reported as a problem, never skipped in silence.

Inputs

Input How it is read
Any file, normally *_agent-artifacts.tar.gz or .zip from either collector Recognised as zip or tar by its content (any compression tarfile reads), extracted to a temporary directory (or --work-dir), then read as an extracted collection. If it does not unpack to manifest.jsonl and fs/, it is read as a loose tree. A .sha256 sidecar next to the archive is verified and the result reported as a note. Symlinks, hard links and device files inside the archive are not materialised (their targets are in the manifest), and members with absolute paths, .. or a drive letter are skipped. A file that is neither tar nor zip is an error.
Extracted collection: a directory holding manifest.jsonl and fs/ Host name from collection.json hostname unless --host is given. User, home and agent for every file come from the manifest. Only rows with status collected and type file are read.
Anything else Treated as a loose tree: a copied home directory, a mounted disk image, another collector's output, or one agent directory such as .claude. Homes and agents are discovered from the catalog, and the user is inferred from the path. A directory with fs/ but no manifest.jsonl is read from fs/.

In a collection, the bases listed in collection.json projects, and the base of any manifest row with agent project, are project directories rather than homes: their files are the collector's project catalog files, and inventory.json marks those paths collected (see inventory.json). A manifest row with agent project, which every row from a project directory had before collector 1.12.0, is re-attributed from the bundled project catalog to the agent that owns the file (see detect).

In loose mode the analyzer walks the tree and treats a directory as a home when at least one catalog entry for a real agent matches under it. A directory that holds only shared files such as AGENTS.md is a project, not a home. A home is never nested inside another, so project-level .claude directories inside a home are not mistaken for a second user. The user is taken from home/<user>, Users/<user>, usr/home/<user>, export/home/<user>, root or var/root in the path, falling back to the directory name, and left empty when the input root itself is the home. When the input is itself an agent directory, its name must match a one-segment catalog entry such as .claude or .codex. Its parent is then the only home, the user comes from the parent's path by the same conventions (with no directory-name fallback), and source_file is relative to that parent. Pass --user and --host to fill in what the path cannot say. Loose-mode output is marked inferred in detect --json because none of this comes from a manifest. Exclusions do not apply in loose mode: every file under a matched catalog path is attributed and offered to the parsers.

Unreadable inputs

An input that cannot be opened stops detect, timeline and inventory with exit code 2 and one line on stderr, error: <path>: <reason>, where the reason is the operating system's message in lower case, such as no such file or directory or permission denied. The path is the file the operating system named, so it is the input itself, or manifest.jsonl inside it, or the archive member being extracted. This covers an input that cannot be stat'ed (including one inside a directory the analyst cannot traverse), an input directory that cannot be listed, an archive that cannot be read or written to the work directory while it is extracted, and a manifest.jsonl that cannot be read. An archive whose content is corrupt gives error: <path>: cannot extract: <reason>. An extracted archive is removed before the command exits.

Anything inside an input that cannot be read is a problem: line and the command carries on:

Problem line When
<directory>: <reason> A directory in a loose tree that cannot be listed, while homes are discovered or files under a catalog match are walked. Reported once per directory; nothing under it is attributed.
<original path>: missing from the collection A manifest row with status collected and type file whose archive_path is not a regular file in the extracted collection, for example in a truncated or edited archive. The row is not attributed. Any other reason the file cannot be stat'ed is printed instead.
<original path>: <reason> timeline only: a parser could not read an attributed file.

detect prints them after the notes, and timeline before its window: line. Symlinks are never followed, so a dangling symlink in a loose tree is skipped like any other symlink rather than reported. An unreadable collection.json is a note, collection.json unreadable: <reason>, and an unreadable .sha256 sidecar is the note WARNING archive sha256 not checked: <path>: <reason>.

Detection

Detection uses the collectors' own catalog. doubleagent/catalog.txt is a verbatim copy of collect-agent-artifacts.sh --list, made with:

collectors/collect-agent-artifacts.sh --list > analyzer/doubleagent/catalog.txt

Every agent the collectors know is therefore detected, whether or not a parser exists for it. When two entries match the same file, the more specific one wins, so Antigravity CLI state under .gemini/antigravity-cli is attributed to antigravity, not to the enclosing gemini-cli entry, in both loose mode and the collectors' manifests. doubleagent catalog --agents lists the agents and which have parsers.

Routing files to parsers

Every file goes to the parsers of the agent the manifest (or, for loose input, the catalog) attributes it to, plus any parser that names that agent in its reads_agents attribute. secret:// rows are never read.

Files the collector finds inside a project directory through its project catalog have the project directory as their manifest home, which the analyzer marks as a project base (see Inputs). From collector 1.12.0 they carry the agent the project catalog names for them, a repository's .aider.chat.history.md aider and its .crush/crush.db crush, and go to that agent's parsers like any other file, so --agent aider and --agent crush include them; the rows of older collectors are re-attributed as Inputs describes. A file the catalog leaves to agent project, because several agents read it (AGENTS.md), is offered to every parser, and the rows of any parser that accepts it are kept under that parser's agent. A parser that scopes a row to the project, such as claude-code for a project's .claude/settings.json, reads the project base marker, not the agent.

A project's git metadata, collected from collector 1.19.0 (see Git metadata in collectors/docs/coverage.md), is in the same group: its rows have agent project, and no parser wants them. The analyzer reads two of those files itself, from a project base only: the remotes of .git/config and the branch or commit of .git/HEAD, which detect --json and inventory.json give per project as remotes and head (see detect --json). One of the two files the analyzer cannot read is a problem: line, as for any other unreadable file (see Unreadable inputs), and leaves the project's remotes empty. A loose input has no project bases, so these stay empty there.

Cody and Twinny keep their chats as rows of the editor's own state.vscdb, which the catalog attributes to vscode or to the fork whose User/ directory holds it (cursor, windsurf, pearai, kiro, antigravity); both parsers list all six editors in reads_agents, so that file is offered to them too and the rows come out under cody or twinny, and --agent cody selects them. The ChatGPT desktop app keeps its list of the account's ChatGPT conversations in .codex/sqlite/codex-dev.db, which the catalog attributes to codex-cli because it sits under .codex; the chatgpt-desktop parser lists codex-cli in reads_agents, so its rows come out under chatgpt-desktop, and --agent chatgpt-desktop selects them.