Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,53 @@ fidelis health
fidelis seed ~/memory/ ~/notes/
```

## Search your Claude Code session history

`fidelis sessions` indexes your past Claude Code conversations (the JSONL files Claude Code writes under `~/.claude/projects/`) so you can search what you actually said and decided across sessions. Same zero-LLM retrieval path - searchable, best on multi-turn queries.

```bash
fidelis sessions ingest # index the last 7 days
fidelis sessions ingest --since 2026-05-01
fidelis sessions ingest --all # index everything
fidelis sessions ingest --dry-run # preview, write nothing

fidelis sessions search "what did we decide about auth"
fidelis sessions search "auth" --limit 5 --raw # JSON output

fidelis sessions list # indexed sessions
fidelis sessions stats # corpus stats

fidelis sessions purge --before 2026-04-01 # remove old sessions from the index
fidelis sessions purge --all --dry-run # preview what would be removed
```

**Deletion stops at the index.** `purge` removes sessions from fidelis's search index only. It never touches your source files in `~/.claude/projects` or backups in `~/Backups/claude-sessions` - those are left alone by design. `purge` prompts before deleting unless you pass `--yes`.

**Pick your privacy posture once.** `FIDELIS_SESSIONS_MODE` chooses how `ingest` decides what to read (set it via the env var, or the `sessions_mode` key in `~/.cogito/config.json` — the env var wins):

- `opt-out` (default): index everything **except** the deny-list. Use `FIDELIS_SESSIONS_EXCLUDE`, a comma-separated list of substrings; any project whose path contains one is never indexed.
- `opt-in`: index **nothing except** the allow-list. Use `FIDELIS_SESSIONS_INCLUDE`, a comma-separated list of substrings; only projects whose path contains one are indexed. With an empty allow-list nothing is indexed and `ingest` tells you to set it.

```bash
# opt-out (default): index all but the named projects
export FIDELIS_SESSIONS_EXCLUDE="client-acme,secrets"
fidelis sessions ingest

# opt-in: index only the named projects
export FIDELIS_SESSIONS_MODE=opt-in
export FIDELIS_SESSIONS_INCLUDE="hermes,langquant"
fidelis sessions ingest
```

### Privacy & storage

- **What is stored, where:** `ingest` writes a copy of your session conversation text into a local ChromaDB store at `~/.cogito/store`. The full per-turn conversation text is retained in metadata (`turns_json`). Embeddings are produced locally by Ollama; nothing is uploaded.
- **How it's protected:** the stored text is **not application-encrypted.** `ingest` sets `chmod 700` on `~/.cogito` (readable only by your OS user); pair that with full-disk encryption (FileVault) for at-rest protection.
- **Deletion stops at the index:** `purge` removes the index records (text, embeddings, metadata) for matched sessions. It never touches your source files in `~/.claude/projects` or backups in `~/Backups/claude-sessions`.
- **Keep sensitive projects out:** in opt-out mode use `FIDELIS_SESSIONS_EXCLUDE` (above) to never index them; or set `FIDELIS_SESSIONS_MODE=opt-in` and name only what you want via `FIDELIS_SESSIONS_INCLUDE` so nothing else is ever read.

See [SECURITY.md](SECURITY.md) for full detail.

Python helper for direct integration:

```python
Expand Down
89 changes: 89 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Security & Privacy

This document covers how fidelis handles your data, with specific detail on the
`fidelis sessions` feature, which indexes your Claude Code conversation history.

## Reporting a vulnerability

If you find a security issue, please open a private report via GitHub Security
Advisories on the repository, or email the maintainers. Please do not file public
issues for vulnerabilities.

## What `fidelis sessions` stores, and where

`fidelis sessions ingest` reads your Claude Code session files
(`~/.claude/projects/*/SESSION_ID.jsonl`) and writes a searchable copy into a
**local** ChromaDB store at `~/.cogito/store`.

Each indexed session is stored with:

- the **full conversation text** of the session's turns, retained in the
`turns_json` metadata field (user and assistant turns, up to 100 turns per
session);
- a flat text representation used as the embedding document and for retrieval
display;
- metadata: session id, project path, turn count, and timestamps.

There are no cloud calls in the default path: embeddings are produced locally by
Ollama (`nomic-embed-text`) and stored locally by ChromaDB. Nothing is uploaded.

A dedup ledger and purge log live alongside the store at
`~/.cogito/session_ingest/`.

## How the stored data is protected

The session text is **not application-encrypted at rest.** It is plain text
inside a local ChromaDB store. Protection relies on:

- **Filesystem permissions.** Ingest sets `chmod 700` on `~/.cogito`, so the
directory is readable only by your OS user.
- **Full-disk encryption (FileVault on macOS, or your platform's equivalent).**
Enable it. It is your at-rest encryption for this data.

If you share an account, run on a multi-user machine, or do not have full-disk
encryption, treat the contents of `~/.cogito` as readable plain text.

## Keeping sensitive projects out (pre-ingest lever)

The honest privacy lever is to **not index** sensitive projects in the first
place. You pick the posture once with `FIDELIS_SESSIONS_MODE` (read from the env
var, or the `sessions_mode` key in `~/.cogito/config.json`; the env var wins).
The default is `opt-out`.

**opt-out (default) — deny-list.** Index everything *except* named projects.
Set `FIDELIS_SESSIONS_EXCLUDE` to a comma-separated list of substrings; any
project whose path contains one of them is never read or indexed:

```bash
export FIDELIS_SESSIONS_EXCLUDE="client-acme,secrets"
fidelis sessions ingest --all
```

**opt-in — allow-list.** Index *nothing except* named projects. Set
`FIDELIS_SESSIONS_MODE=opt-in` and name what to index with
`FIDELIS_SESSIONS_INCLUDE` (same comma-separated substring mechanism); only
matching projects are read. With an empty allow-list nothing is indexed and
`ingest` prints a message telling you to set it (it never silently does nothing):

```bash
export FIDELIS_SESSIONS_MODE=opt-in
export FIDELIS_SESSIONS_INCLUDE="hermes,langquant"
fidelis sessions ingest --all
```

This is also documented in `fidelis sessions ingest --help`.

## Deletion stops at the index

`fidelis sessions purge` removes matched sessions from the search index — their
text, embeddings, and metadata records — and cleans the dedup ledger entry.

Purge **never** touches your original Claude Code session files in
`~/.claude/projects` or any backups in `~/Backups/claude-sessions`. Those are
left alone by design. Purge prompts for confirmation unless you pass `--yes`.

To remove the entire store:

```bash
rm -rf ~/.cogito ~/.fidelis
```
88 changes: 88 additions & 0 deletions src/fidelis/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,69 @@ def main():
p_mcp_uninstall.add_argument("--settings", help="Path to settings.local.json")
p_mcp_uninstall.set_defaults(func=lambda a: sys.exit(_cmd_mcp_uninstall(a)))

# sessions — searchable, purge-able Claude Code session memory (MVP-A)
p_sessions = sub.add_parser("sessions", help="Search/manage your Claude Code session history")
sess_sub = p_sessions.add_subparsers(dest="sessions_command", required=True)

s_ingest = sess_sub.add_parser(
"ingest",
help="Index sessions (default: last 7 days)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=(
"Privacy posture (pick once):\n"
" FIDELIS_SESSIONS_MODE opt-out (default) | opt-in. Also read from\n"
" ~/.cogito/config.json key 'sessions_mode';\n"
" the env var wins. opt-out indexes everything\n"
" except the deny-list; opt-in indexes nothing\n"
" except the allow-list.\n"
"\n"
"Privacy levers:\n"
" FIDELIS_SESSIONS_EXCLUDE comma-separated substring deny-list (opt-out\n"
" mode). Any project whose path contains one of\n"
" these substrings is never indexed (pre-ingest).\n"
" FIDELIS_SESSIONS_INCLUDE comma-separated substring allow-list (opt-in\n"
" mode). ONLY projects whose path contains one of\n"
" these substrings are indexed; empty = nothing.\n"
"\n"
" Examples:\n"
' export FIDELIS_SESSIONS_EXCLUDE="client-acme,secrets"\n'
" fidelis sessions ingest --all\n"
"\n"
' export FIDELIS_SESSIONS_MODE=opt-in\n'
' export FIDELIS_SESSIONS_INCLUDE="hermes,langquant"\n'
" fidelis sessions ingest --all\n"
),
)
g = s_ingest.add_mutually_exclusive_group()
g.add_argument("--since", help="Index sessions since YYYY-MM-DD")
g.add_argument("--all", action="store_true", help="Index all sessions")
s_ingest.add_argument("--dry-run", action="store_true", help="Preview, write nothing")
s_ingest.add_argument("--verbose", "-v", action="store_true")
s_ingest.set_defaults(func=lambda a: sys.exit(_cmd_sessions_ingest(a)))

s_search = sess_sub.add_parser("search", help="Search session history")
s_search.add_argument("query", help="Natural-language query")
s_search.add_argument("--limit", type=int, default=5)
s_search.add_argument("--raw", action="store_true", help="JSON output (no turns)")
s_search.set_defaults(func=lambda a: sys.exit(_cmd_sessions_search(a)))

s_list = sess_sub.add_parser("list", help="List indexed sessions")
s_list.add_argument("--since", help="Only since YYYY-MM-DD")
s_list.add_argument("--limit", type=int, default=50)
s_list.set_defaults(func=lambda a: sys.exit(_cmd_sessions_list(a)))

s_purge = sess_sub.add_parser("purge", help="Delete sessions from the index (NOT source/backups)")
pg = s_purge.add_mutually_exclusive_group(required=True)
pg.add_argument("--all", action="store_true", help="Purge every indexed session")
pg.add_argument("--before", help="Purge sessions older than YYYY-MM-DD")
s_purge.add_argument("--dry-run", action="store_true", help="Preview, write nothing")
s_purge.add_argument("--yes", action="store_true", help="Skip confirmation prompt")
s_purge.set_defaults(func=lambda a: sys.exit(_cmd_sessions_purge(a)))

s_stats = sess_sub.add_parser("stats", help="Corpus stats for indexed sessions")
s_stats.add_argument("--since", help="Only since YYYY-MM-DD")
s_stats.set_defaults(func=lambda a: sys.exit(_cmd_sessions_stats(a)))

args = parser.parse_args()
args.func(args)

Expand Down Expand Up @@ -337,5 +400,30 @@ def _cmd_mcp_uninstall(args):
return cmd_mcp_uninstall(args)


def _cmd_sessions_ingest(args):
from fidelis.sessions_cmd import cmd_ingest
return cmd_ingest(args)


def _cmd_sessions_search(args):
from fidelis.sessions_cmd import cmd_search
return cmd_search(args)


def _cmd_sessions_list(args):
from fidelis.sessions_cmd import cmd_list
return cmd_list(args)


def _cmd_sessions_purge(args):
from fidelis.sessions_cmd import cmd_purge
return cmd_purge(args)


def _cmd_sessions_stats(args):
from fidelis.sessions_cmd import cmd_stats
return cmd_stats(args)


if __name__ == "__main__":
main()
Loading
Loading