Search and resume your AI coding sessions — a unified, full-text searchable index of every Claude Code, OpenAI Codex, Codewith, and Gemini session on your machine.
Documentation: CLI reference · Configuration · Live status contract
bun install -g @hasna/sessionssessions reads the session files written by your coding agents
(~/.claude/projects, ~/.codex/sessions, ~/.codewith/sessions, ~/.gemini), normalizes them into a
single SQLite database, and makes them full-text searchable — across providers,
projects, and time.
# Index sessions into the searchable DB (incremental; skips unchanged files)
sessions ingest # all providers
sessions ingest --source codex # one provider
sessions ingest --source codewith
sessions ingest --force # re-index everything
sessions sync --json # ingest locally; pushes content when self_hosted API env is set
sessions sync --dry-run --json # plan a self_hosted /v1 content push
# Full-text search across every session
sessions search "kubernetes deploy"
sessions search "stripe webhook" --source codex --project app
sessions search "kubectl apply" --tools # search tool calls
# Semantic / hybrid search (run `sessions embed` first; needs OPENAI_API_KEY)
sessions embed
sessions search "how did I fix the auth bug" --semantic
sessions search "auth bug" --hybrid # blend full-text + semantic (RRF)
# High-level recall for coding threads: FTS + optional semantic + tools + graph
sessions recall "find the thread where we implemented stripe webhooks"
sessions recall "resume building the API auth flow" --json
# Knowledge graph — entities (projects/tools/models/repos) and their links
sessions graph # all entities with counts
sessions graph --type tool
sessions graph --related project:infra # sessions in a project
sessions graph --session <id> # a session's neighborhood
# Browse
sessions recent # most recently active sessions
sessions list-indexed --project app # alias: indexed-list
sessions show <id> # full details + message previews
sessions stats # per-source + top-project counts
# Live tmux-backed Codewith/session activity (does not require indexed history)
sessions live --open-only
sessions live --open-only --status active
sessions live --open-only --status idle,dead,needs_attention
sessions live --open-only --json | jq '.[] | {target,status,projectPath,lastVisibleLine}'
sessions bulk status --open-only --status active --json
sessions bulk stop --open-only --status idle,dead --dry-run
# Keep the index continuously fresh (fs.watch + periodic safety re-scan)
sessions watch-ingest
sessions watch-ingest --status
# Keep local changes ready for self_hosted sync (bounded polling; Ctrl-C to stop)
sessions daemon --dry-run --interval 60
sessions sync --watch --interval 60 --max-iterations 3
# Manual refresh / reindex
sessions reindexCodex and Codewith rollout files are enumerated in path order. When multiple files have the same session ID, ingest keeps the snapshot with the most total messages and tool calls; ties prefer more messages, then the newer source modification time, then the lexicographically later path. A stale partial copy therefore cannot replace a fuller snapshot, and live and archived copies share one session row.
sessions list --json
sessions history --today
sessions transcript-search "search every indexed transcript"
sessions rename <id-or-prefix> "a clearer title"
sessions resume --last --print-command
sessions resume <id-or-prefix>sessions list, rename, and resume use the active store: the local SQLite
index by default or the authenticated self-hosted /v1 API when configured.
Only Claude sessions currently produce an executable resume command. Use
sessions list-indexed (alias indexed-list) when you need source and machine
filters in addition to project filtering.
Use sessions live when you need current tmux/Codewith pane state; it reports
active, idle, needs_attention, and dead panes from tmux even when no indexed
session history exists yet.
Use sessions bulk when orchestration needs a guarded JSON plan with active
agent/load hints, concurrency and jitter settings, and explicit refusal reasons.
Mutating bulk execution is currently plan-only: use --dry-run to inspect the
actions that would be taken.
Existing maintenance commands (relocate, transfer, migrate, paths)
remain available.
sessions handoff <target> creates a typed ExternalHandoffBundleV1 JSON file
under ~/.hasna/sessions/handoffs/ for safe slash-command wrappers such as
/handoff codewith.
# Build and write a bundle, then print the Codewith continuation command
sessions handoff codewith --print-command
# Hook-friendly mode: prefer explicit session/transcript hints when available
sessions handoff codewith \
--source-agent claude \
--source-session "$CLAUDE_SESSION_ID" \
--source-transcript "$CLAUDE_TRANSCRIPT_PATH" \
--cwd "$PWD" \
--json
# Preview without writing or launching
sessions handoff codewith --dry-run --json
# Emit installable wrapper skill text named "handoff"; does not write global files
sessions handoff --emit-skill claude
sessions handoff --emit-skill codewith
sessions handoff --emit-skill codex
sessions handoff --emit-skill opencode
sessions handoff --emit-skill cursorThe v1 protocol is deliberately not a live tmux paste. It writes redacted context, recent turns, cwd/repo/git summary, auth/profile references by name only, verification notes, blockers, a bundle hash, and a rendered target command. Source exit is not automatic because v1 has no target acknowledgement protocol.
sessions-mcpExposes session tools for agents/orchestrators: search_sessions,
search_tool_calls, recall_session, semantic_search, recent_sessions,
list_sessions, machines, get_session, ingest, embed, session_stats,
knowledge_graph, active-store tools (sessions_list, sessions_history,
sessions_search, sessions_resume, sessions_rename, sessions_watch,
sessions_watchdog_restart, sessions_watchdog_restart_all, sessions_stats),
cross-adapter import tools, and agent registry tools. MCP no longer exposes the
removed DSN-on-client push/pull tools or direct feedback write tool.
Long-lived Streamable HTTP transport (default port 8877, bind 127.0.0.1 only):
sessions-mcp --http
# or
MCP_HTTP=1 sessions-mcp
# override port
sessions-mcp --http --port 8877
MCP_HTTP_PORT=8877 sessions-mcp --httpEndpoints: GET /health → {"status":"ok","name":"sessions"}, MCP at /mcp.
Uses stateless StreamableHTTPServerTransport (shared process, many clients).
HTTP is the default transport. Use sessions-mcp --stdio or MCP_STDIO=1 for
stdio clients; --http and MCP_HTTP=1 remain available as explicit HTTP
selectors.
By default sessions use the local SQLite index at ~/.hasna/sessions/.
sessions sync ingests local sessions and recomputes machine metadata. In local
mode the on-box index is authoritative, so there is nothing to push or pull.
To share one registry across machines, point the CLI or MCP server at a
self-hosted sessions-serve instance with HASNA_SESSIONS_API_URL and
HASNA_SESSIONS_API_KEY. In that mode sessions sync pushes locally indexed
session metadata and content to the authenticated /v1 API. Clients do not
open a Postgres DSN, and the former client-side storage subcommand family has
been removed.
sessions recall is local-only because its combined FTS, semantic, tool-call,
and graph ranking uses the on-box index. In hosted/self-hosted mode, use
sessions list, sessions show <id>, and sessions search <query> against the
active hosted store instead. Run sessions recall on a machine in local mode
when the richer recall result is required; the CLI and public storage SDK fail
before making a hosted recall request. No /v1/recall endpoint is provided;
generated HTTP SDK consumers can use listSessions, getSession with
listSessionMessages, and searchSessions.
Use API sync when this machine should push local indexed sessions, messages, and
tool calls to the Hasna self-hosted Sessions service over /v1 instead of
writing directly to a database. Configure:
export HASNA_SESSIONS_MODE=self_hosted
export HASNA_SESSIONS_API_URL=https://sessions.your-deployment.example
export HASNA_SESSIONS_API_KEY=...Plan first:
sessions sync --dry-run --json
sessions sync --dry-run --source claude --limit 100Live sync requires a successful --backup-command before it pushes content to
/v1/sessions/import. Use a SQLite-safe export such as VACUUM INTO, the
SQLite backup API, or sessions transfer export; a raw file copy of an active
SQLite DB is only a best-effort snapshot and is not accepted as the built-in
safety gate. The import API refuses, by default, to replace existing session
content with fewer messages or tool calls; intentional pruning must include
destructive.allowContentShrink: true and a non-empty reason in the request
body. Hook output and the raw hook command are suppressed so secrets are not
echoed.
sessions sync --backup-command 'sessions transfer export --output ~/.hasna/sessions/backups'For daemon/watch mode, use bounded polling. Unchanged cycles are suppressed so a
long-running worker does not spam logs. sessions daemon and
sessions sync --watch default to --max-iterations 60; pass an explicit
larger value for a longer supervised run.
sessions daemon --interval 60 --backup-command 'sessions transfer export --output ~/.hasna/sessions/backups'
sessions sync --watch --interval 60 --max-iterations 10 \
--backup-command 'sessions transfer export --output ~/.hasna/sessions/backups'Supervise continuous local ingestion as a long-running service and let its
10-second poll recover writes missed by filesystem notifications. For example,
a systemd user service can run sessions ingest-watch --no-initial --poll 10000
with Restart=on-failure and RestartSec=5; set provider path environment
variables in the supervisor rather than embedding machine-specific paths in the
unit. After starting or restarting it, verify both the configured Codewith root
and persisted ingest health with sessions daemon --status (or --json for a
health probe). A healthy active root has a recent last attempt and success, zero
lag, and no last error; skipped files normally increase on unchanged poll ticks.
To roll back, stop and disable the supervisor unit, restore the previously
installed package version, and run one sessions ingest --source codewith
before re-enabling the old unit. Keep the sessions database and
watch-status.json: ingestion is mtime-gated and session writes are upserts, so
restarts and version rollback do not require deleting state or rebuilding the
index. If the restored version cannot read the existing database, leave the
service stopped and restore the database from the pre-upgrade backup instead.
For one-time historical content backfills, use the explicit backfill workflow instead of an unbounded live sync. It defaults to inventory/dry-run JSON and reports selected sessions, duplicate source IDs, message/tool-call counts, byte estimates, parser memory bounds, and checkpoint state.
sessions backfill --source codewith --pilot 25 --json
sessions backfill \
--source codewith \
--range-start codewith:01aaa \
--range-end codewith:01azz \
--known-id codewith:01abc \
--checkpoint ~/.hasna/sessions/backfill/codewith-range.json \
--jsonLive apply is fail-closed: it requires a self-hosted API store, an explicit
selection boundary (--source plus --pilot, a range, or a --known-id; or
the conspicuous --all-sources acknowledgement), a capacity ceiling, a
successful backup hook, durable checkpointing, and the literal confirmation
token. Production-like API URLs also require --allow-production, but that
flag is only a technical gate: actual production apply still requires separate
out-of-band user approval before running the command.
When --known-id is the only apply boundary beyond --source, only those known
IDs are selected. Do not combine --all-sources with --known-id: that mixes a
broad acknowledgement with a narrow selector and fails closed. An API URL is
only auto-detected as production-like against host suffixes you configure via
HASNA_SESSIONS_PRODUCTION_HOSTS (comma/space separated, e.g. your own root
domain) — this package does not ship a built-in production hostname. If you'd
rather force the gate unconditionally regardless of URL, set
HASNA_SESSIONS_PRODUCTION=1.
sessions backfill \
--apply \
--confirm-apply BACKFILL_APPLY \
--source codewith \
--pilot 25 \
--max-total-bytes 1073741824 \
--backup-command 'sessions transfer export --output ~/.hasna/sessions/backups' \
--checkpoint ~/.hasna/sessions/backfill/codewith-pilot.json \
--jsonRun the service-side Postgres schema with sessions-serve migrate using the
owner DSN. The current server-side storage mode value is
HASNA_SESSIONS_STORAGE_MODE=cloud, but this README uses "self-hosted" for the
deployment mode: the service runs in Hasna-owned infrastructure or your own
server, and clients talk to its /v1 API.
Indexed ingestion currently uses stable local files for Claude Code, local Codex
JSONL, local Codewith JSONL, and Gemini. Cursor/cloud Codex/cloud Claude sources should be added
through the existing SessionParser/SessionAdapter interfaces when they expose
a durable local export or API; avoid scraping transient cloud/cache formats.
sessions-serve exposes unauthenticated health/documentation endpoints and a
versioned, API-key-authenticated /v1 API:
GET /health,GET /ready,GET /version→{ status, version, mode }GET /openapi.json→ OpenAPI 3 document (the SDK is generated from it)/v1/sessions(list/create),/v1/sessions/import(content upsert),/v1/sessions/:id(get/delete),/v1/sessions/:id/messages,/v1/sessions/:id/tool-calls,/v1/search,/v1/recent,/v1/machines,/v1/stats- Additional authenticated server routes:
PATCH /v1/sessions/:id,POST /v1/relocate,GET /v1/search/content,GET /v1/search/tools,GET /v1/graph
Legacy unauthenticated content routes such as /search, /recall,
/tool-calls, /recent, /list, /machines, /stats, and /sessions/:id
are removed and should return 404. Use the /v1 routes with an API key.
Auth uses @hasna/contracts API keys (header x-api-key or
Authorization: Bearer). Set the signing secret with
HASNA_SESSIONS_API_SIGNING_KEY (or the shared HASNA_API_SIGNING_KEY) and
issue keys with bunx @hasna/contracts issue-key --app sessions --scopes 'sessions:read,sessions:write'.
In self-hosted server mode (HASNA_SESSIONS_STORAGE_MODE=cloud +
HASNA_SESSIONS_DATABASE_URL) the service reads/writes Postgres directly: no
client-side DSN sync engine and no service-side cache. Apply the schema with
sessions-serve migrate (run with the owner DSN). See docker-compose.yml for
a self-hosted stack (serve + Postgres) and Dockerfile for the ARM64 image.
Self-hosted mode raises Bun's request body limit to 512 MiB for large
/v1/sessions/import payloads; override with
HASNA_SESSIONS_MAX_REQUEST_BODY_SIZE using bytes or units such as 768MiB.
The generated, dependency-free SDK is published at @hasna/sessions/sdk:
import { SessionsApi } from "@hasna/sessions/sdk";
const client = new SessionsApi({ baseUrl: process.env.SESSIONS_API_URL!, apiKey: process.env.SESSIONS_API_KEY });
const { sessions } = await client.listSessions({ limit: 20 });Data is stored in ~/.hasna/sessions/ (sessions.db).
Apache-2.0 -- see LICENSE