> ## Documentation Index
> Fetch the complete documentation index at: https://comis-fix-skill-import-vetting-gate.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Data Directory

> A complete reference for everything Comis keeps in ~/.comis/ — what each file is, when it's written, and what's safe to delete.

Here's everything Comis keeps on disk — and why.

## Overview

`~/.comis/` is the single directory where Comis stores all runtime state: your config, secrets, conversation memory, logs, and the pm2 supervisor file. It is created automatically the first time you run `comis init` or start the daemon.

<Note>
  The opt-in [Prometheus & Grafana](/operations/prometheus-grafana) surface adds **no
  file** to `~/.comis/` — it opens a loopback network listener (`/metrics` on
  `127.0.0.1:9464`), not a data-dir path. The Grafana/Prometheus artifacts
  (dashboards, rules, docker-compose) live in the Comis source repository, not the
  data directory.
</Note>

## Directory layout

```
~/.comis/
├── config.yaml              # your main configuration file
├── config.last-good.yaml    # last successful startup snapshot
├── config.local.yaml        # optional local overrides (not committed)
├── .env                     # secret overrides (LLM keys, tokens)
├── memory.db                # SQLite — conversations, memory, sessions
├── secrets.db               # SQLite — AES-256-GCM encrypted secrets (encrypted mode)
├── auth-profiles.json       # OAuth profiles — file mode only (0o600)
├── mcp-tokens/              # MCP OAuth tokens — file mode only (per-server JSON)
│   └── <server-name>.json
├── secrets.json             # secret store — file mode only (0o600)
├── .daemon.lock             # exclusive lock held while daemon is running
├── daemon.console.log       # raw daemon stdout/stderr capture (CLI launcher only)
├── broker.sock              # Unix socket for credential broker (when active)
├── cap.sock                 # Unix socket for the capability endpoint (0600; when an autonomy-bearing agent is configured)
├── ecosystem.config.js      # pm2 process definition (created by pm2 setup)
├── logs/
│   ├── daemon.N.log         # structured JSON log (pino-roll; live file = newest N)
│   └── security-audit.jsonl # durable security-decision audit trail (0600, rotated; one scrubbed JSON line per event)
├── traces/                  # per-session JSONL execution traces
├── support-bundles/         # comis support-bundle output — one comis-support-<ts>/ dir per run
├── workspace/               # default agent workspace (files, git, skills, projects/)
│   └── trace-exports/       # comis trace export output — one comis-trace-<sid8>-<ts>/ dir per export
├── workspace-<agentId>/     # named-agent workspaces (one per extra agent)
├── terminal-drive/          # durable terminal-driver state (default; absent only with drive.durable: false)
│   └── <agentId>/journals/<sessionId>.json
├── models/                  # auto-downloaded ML model caches
│   └── whisper/             # local STT whisper model (downloaded once on first use, reused)
├── agents/                  # per-agent RPC state (subagent results, etc.)
├── background-tasks/        # background task state files
├── skills/                  # seeded skill library — bundled claude-code/codex/skill-creator skills (re-seeded on version change) + any added skills
│   └── <name>/SKILL.md      # one dir per skill (the agent auto-discovers these by description)
├── installed-skills.json    # skill provenance — origin, content hash, trust tier (0o600)
├── identity/
│   └── device.json          # Ed25519 device identity (auto-generated)
└── devices/                 # paired device records (if pairing is used)
```

<Note>
  Some entries only appear under specific conditions.

  * `secrets.db` — created only when `SECRETS_MASTER_KEY` is set and `security.storage: encrypted` (the default).
  * `auth-profiles.json`, `mcp-tokens/`, `secrets.json` — only present when `security.storage: file`. In `encrypted` mode, OAuth profiles and secrets live in `secrets.db` tables instead.
  * `.daemon.lock` — present only while the daemon is running. Safe to delete when the daemon is stopped (it will be recreated on the next start).
  * `installed-skills.json` — created on the first skill install; absent on a daemon that has never installed one.
  * `daemon.console.log` — present only when the daemon was launched by the CLI (the `comis init` wizard's start step or `comis daemon start`). Under systemd the process output goes to the journal instead, and under pm2 to `~/.pm2/logs/` — so this file is absent in those setups.
  * `broker.sock` — present only when the credential broker is active (`executor.broker.enabled: true` in `config.yaml`). Ephemeral — deleted and recreated on each daemon start.
  * `cap.sock` — present only when an autonomy-bearing agent is configured (the loopback capability endpoint). Ephemeral — `0600` owner-only, deleted and recreated on each daemon start. The lease **bearer** itself is never persisted — it is re-minted on each resume from the run's persisted (attenuated) capabilities and exact trust level, so there is still no `cap.db` / lease-secret table on disk. The transient [bounded-autonomy](/agents/autonomy) control-plane *counters* (the per-root spawn-ceiling counters, the call-rate windows, and the outward-quota windows) remain **in-memory** and reset on restart. The in-flight **run state** need not be ephemeral, though: when [`autonomy.durability.enabled`](/reference/config-yaml#autonomy-agentsautonomy) is on (**on by default** — full capability out of the box), the daemon persists each run's checkpoint and its outward-send ledger to `memory.db` (the [`durable_run_checkpoints` + `outward_send_ledger`](#memory-db) tables), so a cron-fired pipeline **survives a daemon restart** — on resume the lease is re-minted from those persisted attenuated caps and trust level, and the consumed budget is re-seeded from the record. With durability off (an explicit opt-out) the data-dir layout is unchanged by autonomy and those tables are never written. See [Durability & Resume](/agents/autonomy#durability-resume).
  * `ecosystem.config.js` — created only by `comis pm2 setup`.
  * `support-bundles/` — created only after the **first** `comis support-bundle` run; each run writes one timestamped `comis-support-<ts>/` directory. Absent on a fresh install.
  * `workspace/trace-exports/` — created only after the **first** `comis trace export` (or an owner `/export-trajectory`); each export writes one `comis-trace-<sid8>-<ts>/` directory under the agent's workspace. Absent until then.
  * Named workspaces like `workspace-myagent/` — appear only when you define more than one agent.
  * `terminal-drive/` — the per-agent **durable** terminal drive-state journals. A durable drive is the **default** (`drive.durable: true`), so this appears whenever an agent runs a terminal drive; absent only for an explicit `drive.durable: false` (non-durable) drive or on a tmux-less host.
  * `models/whisper/` — created only after the **first** voice transcription that uses the keyless in-process local STT engine (when `integrations.media.transcription.provider` resolves to `local` without a `local.baseUrl` server). Absent on a fresh install and whenever local STT never runs.
  * `models/tts/` — created only after the **first** voice reply that uses the keyless in-process offline TTS engine (when `integrations.media.tts.provider` resolves to `local`/`piper`). A separate subdir from `models/whisper/`. Absent on a fresh install and whenever local TTS never runs.
</Note>

## File-by-file reference

### `config.yaml`

Your main configuration file. The daemon reads it at every startup. You edit this directly or through `comis config set`.

**Written by:** `comis init` wizard, or you manually.
**Safe to delete?** No. Without it the daemon will not start.

***

### `config.last-good.yaml`

A copy of `config.yaml` snapshotted after every successful daemon startup. If the daemon crashes on boot, this file lets you roll back.

**Written by:** the daemon automatically, after logging "Comis daemon started".
**Safe to delete?** Yes. The next successful start re-creates it. You lose the ability to roll back until then.

To restore manually:

```bash theme={}
cp ~/.comis/config.last-good.yaml ~/.comis/config.yaml
```

***

### `config.local.yaml`

Optional second config file loaded after `config.yaml`. Values here override the main file. Useful for machine-specific settings you don't want in version control.

**Written by:** you, manually.
**Safe to delete?** Yes. The daemon falls back to `config.yaml` alone.

***

### `.env`

Secret environment variables — LLM API keys, channel tokens, `SECRETS_MASTER_KEY`. The daemon loads this file before anything else, then scrubs the values from `process.env` so child processes cannot inherit them.

**Written by:** `comis init` or you manually.
**Safe to delete?** Only if you have all credentials stored in `secrets.db` or injected via environment instead. Deleting it means the daemon will not have access to any keys defined here.

<Warning>
  Never commit `.env` to version control. It contains unencrypted credentials. Comis sets its file mode to `0o600` on startup.
</Warning>

***

### `memory.db`

The main SQLite database. It stores conversation sessions, long-term memory entries, delivery queue state, observability rows, DAG context engine tables, embedding cache, and more. This is the most important file in the data directory.

The observability rows live in the `obs_diagnostics` table, grouped by a `category` label. Among them, `model_health` is a one-shot diagnostic recorded once per boot that captures the daemon's load-level recall signals — whether an embedding provider is available, whether the GGUF reranker loaded, and whether the reranker model is present on disk. It carries booleans only (no model paths, no secrets) and lets the system health lens explain why recall was degraded on a given daemon without grepping the log.

`config_posture` is likewise a one-shot snapshot recorded once per boot, capturing the daemon's security posture — whether the gateway is running without TLS, the per-family **counts** of stranded secrets (credentials left in the inactive store that the active `security.storage` mode cannot reach), and a canary-fallback aggregate (how many agents fall back to deterministic canary tokens because `CANARY_SECRET` is unset). It records counts and closed labels only — **never a secret value** — so the system health lens can surface a TLS-off / stranded-secret / canary posture without opening the log.

#### Video Job Store — Async Render Lifecycle

`memory.db` also contains the **`video_jobs`** table: the durable record of in-flight and completed `video_generate` renders (it lives in the shared `memory.db`, not a separate database file). A video render takes 30 s–5 min — longer than an agent turn and longer than the `promptTimeout` — so each job is persisted on submit (the opaque provider `jobId`, the routing — agent, channel, `traceId`, and the originating `session_key` — the state `pending`/`done`/`failed`, the estimated/actual cost, and the persisted `media_path` once delivered) and a background poller drives it to completion off-turn. The table is what the poller **resumes against across a daemon restart**: on boot it reloads the `pending` jobs and continues polling, downloads each finished clip before its provider URL expires, persists it to `workspace/media/videos/`, and delivers it to the originating channel — even though the turn that requested it is long gone. It carries **no secret** (the `jobId` is the opaque provider request id; the credential stays with the boot-bound adapter). Reads are agent-scoped, so the `video_status` tool can only return a job to the agent that created it. See the [`video_generate` and `video_status` tools](/agent-tools/media).

The **`session_key`** column ties a job that completes in the *background poller* — after the originating turn has ended — back to that turn, so `comis explain <sessionKey>` can reconstruct the full video turn (provider, model, `jobId`, estimated + actual cost, outcome) including the off-turn completion. It is captured from the request at submit alongside `trace_id`.

#### Durable Runs + Outward-Send Ledger — Durability/Resume

`memory.db` also contains the **`durable_run_checkpoints`** and **`outward_send_ledger`** tables (the durability/resume engine, gated behind [`agents.<id>.autonomy.durability.enabled`](/reference/config-yaml#autonomy-agentsautonomy) — **on by default**, so an autonomy-bearing agent writes them unless durability is explicitly disabled). `durable_run_checkpoints` is the per-root-run checkpoint a long-running autonomy run stamps as its spawn tree advances: the `root_run_id`, the exact `trust_level`, the **attenuated** capabilities the run was minted with (never a superset), the correlated `lease_ids`, the spawn tree, the consumed budget, the cron origin, a `status` (`running`/`orphaned`/`completed`/`revoked`), a `last_heartbeat_at`, and a dedicated monotonic `outward_step` counter. On a daemon **restart** the resume engine scans the `running` rows (after channels are up), re-mints each run's lease from the **persisted caps and trust level verbatim** (never re-attenuating from a live parent — that would broaden authority), re-anchors it under the bounded-autonomy meter, and orphans (never silently re-mints) any run whose capabilities or trust level fail the membership parse or whose lease was revoked. A daemon-wide watchdog sweeps runs whose heartbeat has lapsed past `durability.staleHeartbeatMs`.

`outward_send_ledger` is a closed five-state (`send_attempt_started`, `unknown_after_send`, `committed`, `failed`, or `unresolved`) duplicate-suppression and uncertainty record for outward `message.send`/`reply`/`react` operations. The helper tables `outward_send_sequences` and `outward_send_operations` resolve one caller-created logical operation identity to a stable `(root_run_id, step_index)` pair; the ledger enforces that pair as unique. A retained committed operation returns its stored platform receipt without another send, while any other existing row blocks a second execution. Every send whose platform outcome may be ambiguous is parked `unresolved` and escalated. Startup parks all remaining `send_attempt_started` and `unknown_after_send` rows without querying channel history or replaying them. The records carry content-free SHA-256 digests and routing/lifecycle fields only — no message body or secret. This prevents automatic duplicate execution for the same retained identity but is not a universal exactly-once guarantee. A **revoke** (`lease.revoke`/`run.kill`) flips the persisted run record to `revoked`, so a subsequent boot can never resurrect pre-revoke capabilities. These tables are additive and forward-only (`CREATE TABLE IF NOT EXISTS`), created on every boot.

When [`autonomy.durability.orchestrateResume`](/reference/config-yaml#autonomy-agentsautonomy) is on (default-on, **nested inside** the durability block), the `orchestrate` runner becomes the first **re-runnable** durable kind and its `durable_run_checkpoints` row carries two extra nullable pointers: `script_ref` (the pinned `<runId>.<language>` script at the run-workspace root) and `checkpoint_ref` (the last `checkpoint()` blob — a longer-TTL `kind:"json"` `ResultRef` under the run's `results/`). Both are content-free path/id pointers — **never** the script bytes or the checkpoint body. On a **timeout** of a resume-enabled run the runner re-affirms the row resumable and **skips** the run-end `results/` cleanup so the pinned script + checkpoint survive; `orchestrate({resumeRunId})` then re-spawns the **pinned** bytes (a resume never accepts a re-supplied script). On restart the boot sweep **verifies** the pinned script + checkpoint are still on disk and surfaces the run as resumable — it does **not** re-execute the bytes on boot; a missing artifact is an honest `durable:orphaned` (a closed-enum reason, no free text on the event) and the orphan sweep **reclaims** the run's surviving `results/` + pinned script. The on-disk layout of those artifacts is under [`workspace/`](#orchestrate-resumable-run-artifacts).

#### Outcome Events Ledger: Verified Learning

`memory.db` also contains the **`outcome_events`** table: the durable,
append-only ledger of outcome observations. Every row records one source (`tool`
/ `pipeline` / `correction` / `judge` / `reaction` / `explicit`) for one
trajectory, with a closed `outcome` (`success` / `failure` / `corrected` /
`unknown`), confidence, and `(tenant_id, agent_id, session_id, trajectory_id)`
scope. It is content-free: ids, closed enums, counts, and numeric confidence
only. No message body is stored. Every statement filters on `(tenant_id,
agent_id)`, so one tenant and agent scope cannot read another's rows.

The ledger is age-pruned at daemon startup to the longest configured
`agents.<id>.learningOutcome.retentionDays` (default **90** days). Pruning runs
even when capture is disabled so an existing ledger stays bounded. Rows are
idempotent on `(tenant_id, agent_id, trajectory_id, source, observed_at)`.
Capture is **default-ON** per agent and force-disabled by `memory.enabled:
false`. `comis explain` shows the resolved session signal, and `comis memory
learning` aggregates it. A resolved label is evidence, not independent proof of
the overall task outcome; mixed signals can resolve `success` for a session that
failed overall.

#### Learned Skills: Verified Learning

`memory.db` also contains the **`mental_models`** table, the durable store for
admitted `skill`, `profile`, and `topic` documents. A skill row contains advisory
Markdown and optional required-tool names distilled from trajectories whose
configured outcome resolver reported `success`. That fused label is not proof
that the overall task succeeded.

Every document is forced to `trust_level = 'learned'` by code and a database
check. This prevents a synthesized document from claiming the `system` tier, but
`learned` is still a policy label, not a guarantee that the content is reliable.
The static validator rejects critical secret and poison patterns. Warning-level
patterns, including some jailbreak-like language, can pass validation.

Lifecycle state is `candidate`, `active`, `stale`, or `archived`. Read-only,
non-evicted candidates and active docs can both surface in
`<available_skills>` and under `.learned-skills/<name>/SKILL.md`. Candidates
surface before promotion so reuse can be attributed. Attributed reuse labeled
`success` increments `proof_count` and promotes at `promoteAtProofCount`.
Corroborated failure plus a weakening trend can demote a candidate or active doc
to `stale`; soft eviction sets `evicted_at` and `archived` without deleting
provenance. Mutating, stale, archived, and evicted docs do not surface.

All reads and writes filter on `(tenant_id, agent_id)`. This is not a per-chat
or per-sender storage boundary. Rows are idempotent within that scope. Learning
is default-ON per agent and force-disabled by `memory.enabled: false`. Learned
docs have no executable column and grant no new tool, credential, capability,
approval, or sandbox authority. The admission funnel is available through
[`comis memory skills`](/reference/cli#comis-memory), and per-session attribution
is available through [`comis explain`](/reference/cli#comis-explain).

#### Ranking & Forgetting: Verified Learning

`memory.db` also carries recall-utility counters and outcome-aware forgetting
state. Both are part of the learning layer, which is **default-ON** per agent and
force-disabled by `memory.enabled: false`. Recall ranking itself remains the
fixed `rag.scoring` fusion; there is no learned recall weight. See
[Learning in config-yaml](/reference/config-yaml#learning-agents-learning).

* **`memory_usefulness`** -- per-memory `used_count`, `ignored_count`, and
  `failure_count` counters. A memory recalled but not attributed accrues
  `ignored_count`, not `failure_count`. Failure increments require two distinct
  session observations or one deterministic tool or pipeline source. Every key
  begins with `(tenant_id, agent_id)`.
* **`evicted_at`** (on the `memories` rows) — **soft, reversible eviction.** When forgetting is enabled, the lifecycle sweep marks a low-strength memory (its strength lowered by the bounded `failurePenalty` × `failure_count` coupling) by **setting `evicted_at`** — it is **never hard-deleted**. An evicted memory is **excluded from every recall path** (an always-applied `evicted_at IS NULL` filter) yet stays **resolvable via `asOf` / inspect / audit** reads, and can be **un-evicted** (cleared back to `NULL`) on renewed usefulness. High-corroboration memories are **exempt** and never soft-evicted regardless of strength: `pinned`, `trust_level='system'`, or a high `proof_count`. There is **no `halfLifeDays` knob** (forgetting reuses the existing score-decay; the learning layer adds eviction + wrongness, not a new decay knob).

#### Per-user profile as a Mental Model document

The per-user profile is a `kind:"profile"` document in the `mental_models`
store; there is no standalone profile table. When an accepted profile update
supersedes the prior document, the previous body is appended to `history` rather
than hard-deleted. Recording a correction signal alone does not guarantee that
this update occurs or that future behavior changes. Source eligibility,
reflection, validation, and admission must all succeed. The store boundary
remains `(tenant, agent)` even though profile rows are grouped by user inside
that scope.

#### LCD Store — Durable Conversation History

`memory.db` also contains the **LCD store**: the durable, lossless conversation history. Three layers work together, each with a distinct durability contract:

| Layer                 | Tables                                                                    | Durability                                                                                                                                                                                                                                                                    |
| --------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **LCD store**         | `lcd_messages`, `lcd_message_parts`, `lcd_context_items`, `lcd_summaries` | **Durable — the truth.** Every turn keeps its structured messages and tool-call/result pairing after mandatory secret redaction (auto-condensed past 75%). Survives daemon restarts, redeploys, and JSONL deletions.                                                          |
| **RAG memory**        | `memory_*`                                                                | **Durable.** Distilled facts learned from conversations. Cleared per-session via `comis sessions reset --memory` (deletes paired + LCD-distilled memories by `source_session_key` and unlinks consolidated observations; `--purge-derived` also purges derived observations). |
| **SDK session JSONL** | `~/.comis/workspace/sessions/.../<key>.jsonl`                             | **Disposable and owner-only (`0600`)** — the SDK's per-session working transcript after mandatory secret redaction. Existing nested session artifacts are hardened at daemon startup. Routinely deleted by the operator (housekeeping) and wiped by `comis sessions reset`.   |

<Note>
  The SDK session JSONL is **disposable**. Deleting it directly (for any reason — operator housekeeping, `rm`, daemon restart) is always treated as "continue": the LCD store keeps its full history and appends the fresh transcript as a continuation. The daemon cannot distinguish disk-space housekeeping from a deliberate reset, so it never infers forget from a file deletion — it always continues. To perform an explicit reset, use `comis sessions reset` which clears both layers atomically.
</Note>

**Bootstrap crash recovery.** Each turn's messages are written to the SDK session JSONL first and persisted to the durable LCD store at the end of the turn. If the daemon is killed mid-turn — after the JSONL write but before that end-of-turn persist — those last messages would otherwise never reach the durable store. To close that gap, on **session start** the daemon runs a crash-recovery sweep: it continue-appends any live-transcript messages that the previous turn did not persist, so the next turn sees the complete history. The sweep is **exactly once** — a durable per-conversation cursor records how far the store has caught up, so the message recovered at startup is never re-appended by the turn that follows (no gap, no duplicate, no sequence collision). It is a one-way catch-up: the LCD store is the source of truth and the disposable JSONL is only read, never used to overwrite stored history. If the transcript's identity is ambiguous (a malformed or mismatched session scope), the sweep **refuses the import** rather than risk re-attaching a transcript to the wrong conversation (fail-closed). There is no configuration key — the sweep always runs in dag mode.

**Forgetting is explicit.** To clear a conversation so the model starts with a clean slate in **both dag and pipeline modes**, use the complete reset command:

```bash theme={}
# Reset a conversation (clears LCD history + session transcript — irreversible):
comis sessions reset <sessionKey> --yes
# Also clear RAG memories for this session (the full-forget path):
comis sessions reset <sessionKey> --memory --yes
# Nuclear forget — also purge consolidated observations derived from this session:
comis sessions reset <sessionKey> --memory --purge-derived --yes
```

`sessions reset` clears two layers atomically: the LCD lossless-store history (dag mode) and the daemon sessionStore working transcript (both modes). A follow-up turn has no prior context regardless of which context engine the session uses.

See [CLI Reference](/reference/cli#comis-sessions) for the full command reference.

**Written by:** the daemon continuously during operation.
**Safe to delete?** Deleting it erases all conversation history, stored memories, pending deliveries, and cached embeddings. The daemon will recreate an empty database on next start, but you lose all data permanently.

***

### `secrets.db`

An encrypted SQLite database for secrets managed with `comis secrets set`.
Each value is encrypted with AES-256-GCM using the key in
`SECRETS_MASTER_KEY`. Encrypted storage is the default; when the key is absent,
first boot generates it and writes it to the owner-readable `~/.comis/.env`
before opening this database.

**Written by:** `comis secrets set` and the daemon on startup.
**Safe to delete?** Only if you have not stored any secrets here, or you have them backed up elsewhere. Deleting it with a valid master key means those secrets are gone.

***

### `ecosystem.config.js`

The pm2 process definition, generated by `comis pm2 setup`. It tells pm2 where the daemon binary lives, what environment to pass (`COMIS_CONFIG_PATHS`), and restart behavior.

**Written by:** `comis pm2 setup`, one time.
**Safe to delete?** Yes, if you re-run `comis pm2 setup` afterward. pm2 will not be able to start the daemon until you do.

***

### `logs/daemon.log`

The daemon's own **structured JSON** log, written via pino-roll. The **live file always carries a numeric index** — `daemon.1.log` on a fresh boot, advancing to `daemon.2.log`, … when the current file reaches 10 MB (configurable via `daemon.logging.maxSize` and `daemon.logging.maxFiles`); pino-roll never writes the bare `daemon.log` name, so a `daemon.log` you see in the directory is a stale artifact, not the live log. The newest `daemon.N.log` is the file to tail; aged siblings are gzipped to `daemon.N.log.gz` by the startup rotation sweep (the sweep never touches the newest uncompressed indexed file). This is the canonical application log — leveled lines with `traceId`, `agentId`, `durationMs`, etc. — and the file you parse to diagnose behaviour. The base path is set by `daemon.logging.filePath` (default `~/.comis/logs/daemon.log`).

**Written by:** the daemon continuously.
**Safe to delete?** Yes. The daemon creates a new file on next start. You lose historical log data. Under pm2, logs are also captured to `~/.pm2/logs/` as a separate copy.

<Tip>
  Under pm2, Comis skips the stdout transport to avoid duplicating entries. The canonical structured log lives under `~/.comis/logs/` — the newest `daemon.N.log` is the active file.
</Tip>

***

### `logs/security-audit.jsonl`

The **durable security-decision audit trail** — one scrubbed JSON line per audit event (secret access, injection detection, command blocks, canary leaks, sandbox-downgrade refusals, and classified `audit:event` actions). Created with `0600` permissions and rotated under `observability.logRotation` (size + keep-count). Content-free by construction: ids, the closed-enum fields, and a scrubbed `refs` blob — **never a secret value**. It is the greppable companion to the durable `obs_audit_events` SQLite table that `comis security audit-log` / `obs.audit.query` read; both survive a daemon restart. See [Audit Logging](/security/audit).

**Written by:** the daemon, on each audit event (when `observability.audit.persist` is on, the default).
**Safe to delete?** Yes, but you lose the on-disk audit history (the structured `daemon.N.log` `.audit()` lines and any log-aggregation copy remain). The SQLite rows are unaffected.

***

### `daemon.console.log`

The **raw stdout/stderr** of the detached daemon process, captured by the CLI that launched it. Distinct from `logs/daemon.log`: it is not structured and not written by the daemon's logging subsystem — it lives at the data-dir root alongside `daemon.pid`/`.daemon.lock` as a process-lifecycle artifact. Its value is capturing output that the structured logger never sees: early-boot progress (e.g. embedding-model download), native-library warnings, and `FATAL: Bootstrap failed` crashes that exit *before* the logger is configured. `comis daemon logs` tails this file (override with `COMIS_LOG_PATH`).

**Written by:** the `comis init` wizard's "start daemon" step and `comis daemon start`. Absent under systemd (output → journal) and pm2 (output → `~/.pm2/logs/`).
**Safe to delete?** Yes. Recreated on the next CLI launch.

***

### `traces/`

Per-session JSONL execution traces, rotated at 5 MB per file (3 files kept by default). Controlled by `daemon.logging.tracing`. Each agent can override the output directory in its own config block.

**Written by:** the daemon on each LLM call (when tracing is enabled).
**Safe to delete?** Yes. The daemon creates new trace files as needed.

***

### `support-bundles/`

Output of [`comis support-bundle`](/reference/cli#comis-support-bundle). Each run writes one timestamped `comis-support-<ts>/` directory containing `issue-summary.md`, `ai-issue-draft.md`, `triage.json`, `doctor.json`, and `manifest.json`, plus — when their source can be read — `system-health.json`, `config-posture.json`, and an `audit-summary.json` window digest (a thrown system read, an unparseable config, or an absent observability store omits the corresponding file). Focusing the bundle on one session adds `explain.json` (with `--session`) and, with `--deep`, a `trace-exports/` subdirectory holding that session's redacted trace bundle — so a run writes up to nine files plus the optional `trace-exports/` directory. It is a content-free, paste-ready diagnostic bundle assembled offline. The directory is created with mode `0o700` and every file with `0o600`; the name carries a timestamp only (no hostname). The bundle excludes secrets, message bodies, and raw config values by construction.

**Written by:** `comis support-bundle`, on each run.
**Safe to delete?** Yes. Bundles are point-in-time diagnostic artifacts — delete them once you have shared or triaged them (the privacy guidance is to delete after triage anyway).

***

### `workspace/` and `workspace-<agentId>/`

Each agent gets a dedicated workspace directory. The default agent uses `workspace/`; additional named agents get `workspace-<agentId>/`. Inside each workspace:

* Agent identity files (`AGENTS.md`, `SOUL.md`, `USER.md`, `IDENTITY.md`)
* A `.scheduler/` subdirectory with `cron-jobs.json` and execution logs
* A `skills/` subdirectory for agent-specific prompt skills
* A `projects/` subdirectory — the persistent working root for [terminal-driver](/agent-tools/terminal-driver) sessions (present only if the agent uses that tool). Each named coding project gets its own auto-created folder `projects/<name>`, so a driven session's work (e.g. a Claude Code build) lands at `workspace/projects/<name>` and survives the session. This `projects/` subtree is the only part of `~/.comis` re-exposed read-write inside the session's bwrap jail; the agent's sibling `sessions/`, `skills/`, `memory.db`, and secrets stay masked.
* Other subdirectories seeded by Comis: `scripts/`, `documents/`, `media/`, `data/`, `output/`

The seeded `media/` subdirectory is scratch space for agent file operations and the durable home for generated images. The `image_generate` tool persists each generated image to `media/photos/` inside the agent's workspace (`~/.comis/workspace/media/photos/` for the default agent) via the media-persistence service — a UUID filename with a MIME-detected extension, bounded by the media-persistence size cap. The persisted file is then delivered to the channel via the adapter's `sendAttachment` from that durable path. When the channel cannot receive an attachment — an adapter without `sendAttachment` (currently only IRC) — or no channel is present, the handler returns a bounded base64 image over the RPC response instead. The persisted file is not deleted after delivery, so generated images survive for later inspection (see the [`image_generate` tool](/agent-tools/media)).

The `video_generate` tool persists generated videos to `media/videos/` inside the agent's workspace (`~/.comis/workspace/media/videos/` for the default agent) the same way — a UUID filename via the media-persistence service, with a raised size cap appropriate for video. Because a video render outlives the turn, this persist-and-deliver step runs in the **background poller** (not the originating turn): when the render completes, the poller downloads the clip to a buffer and persists it **before** any delivery decision (an expiring provider URL cannot orphan it), then delivers it as a `video` attachment via `sendAttachment` from that durable path; a channel without attachment support (currently only IRC) degrades to a notice plus the persisted path. Persisted videos are not deleted after delivery (see the [`video_generate` tool](/agent-tools/media)). The in-flight/completed render jobs themselves are tracked in the `video_jobs` table in [`memory.db`](#memory-db) — that is what the poller resumes against across a daemon restart.

#### Orchestrate resumable-run artifacts

An [`orchestrate`](/agent-tools/orchestrate) run uses the agent workspace directly: the runner writes the model's script to the workspace **root** as `<runId>.<language>` and materializes every intermediate result under `results/` as a size-capped, TTL'd `ResultRef` (sliced in-jail, so only the queried slice re-enters context — the full payload never does). Two of those artifacts become **durable** when [`autonomy.durability.orchestrateResume`](/reference/config-yaml#autonomy-agentsautonomy) is on:

* a `checkpoint(stateJson)` blob is a longer-TTL `kind:"json"` `ResultRef` under `results/` (the extended TTL outlasts a full run so a normal run-length GC never evicts it); `resume()` reads the last one back **wrapped as untrusted data** (never evaluated as control);
* each side-effecting cap-socket call appends one **content-free** line to the run-scoped `results/replay.jsonl` — `{ seq, method, paramsDigest (sha256), result: <ResultRef pointer> }`, digests + on-disk pointers only, **never** a raw param, body, or bearer.

A resumable **timeout** skips the run-end `results/` wipe, so the pinned `<runId>.<language>` script, the checkpoint, and `replay.jsonl` all survive for a later resume or replay; the orphan sweep reclaims them once the run is orphaned or its checkpoint TTL-expires. This is **on by default** — with `orchestrateResume` off, an orchestrate run cleans its `results/` at run end. `comis orchestrate replay <runId>` re-runs the pinned bytes against a **separate**, operator-only replay socket that serves those recorded results back → byte-identical stdout (see [`comis orchestrate replay`](/reference/cli#comis-orchestrate)).

**Written by:** the daemon on first startup for each agent.
**Safe to delete?** Deleting a workspace erases the agent's identity files, cron job definitions, and skill configurations. The daemon recreates the directory structure but not the content you or the agent wrote there.

***

### `workspace/trace-exports/`

Output of [`comis trace export`](/operations/incident-bundle) and the owner `/export-trajectory` slash command. Each export writes one `comis-trace-<sid8>-<ts>/` directory **under the agent's workspace** (`workspace/trace-exports/` for the default agent, `workspace-<agentId>/trace-exports/` for a named one), holding the redacted per-session incident bundle. The directory is created with mode `0o700` and every file with `0o600`.

**Written by:** `comis trace export` and the `/export-trajectory` owner command.
**Safe to delete?** Yes. Exports are point-in-time diagnostic artifacts; delete them after handing off to an engineer.

***

### `installed-skills.json`

Provenance for every installed skill: where it came from, the content hash of the bytes that were installed, the trust tier it was assigned, and the vetting verdict at install time. Written after a skill install succeeds — never before, so the file only ever describes skills that actually exist on disk. Created with mode `0o600` (daemon-private).

One entry per skill, keyed `<scope>:<name>` so a `local` and a `shared` skill of the same name stay independent:

```json theme={}
{
  "local:my-skill": {
    "source": "github",
    "ref": "https://github.com/owner/repo/tree/main/skills/my-skill",
    "contentHash": "sha256:...",
    "importedAt": "2026-07-26T10:00:00.000Z",
    "importedBy": { "agentId": "default", "userId": "operator" },
    "trust": "community",
    "verdict": "safe",
    "findingCounts": { "critical": 0, "warn": 2 }
  }
}
```

`source` records which install path produced the skill (`create`, `update`, `upload`, or a remote source). `ref` is a public locator and is absent for locally-authored skills — it never carries credentials. `trust` is **derived from the install call**, never declared by the skill itself: a remote import is always `community`, local authoring by the default agent is `operator`, and only the daemon's own seeding is `first-party`. See [Security Scanning](/skills/security-scanning) for how the verdict is produced.

The record is content-free by construction — a hash and counts, never scanned text — so it is safe to read, log, and paste into a review.

<Note>
  Deleting a skill drops its entry. If you remove a skill directory by hand, the entry is left behind and a later re-install of that name is compared against a hash that no longer corresponds to anything on disk; delete the entry to clear it.
</Note>

A missing or malformed file reads as empty rather than failing startup — provenance is evidence, not a lock.

### `terminal-drive/`

Durable drive-state journals for the [terminal-driver](/agent-tools/terminal-driver), written **only** when an agent runs a durable drive (`drive.durable: true`). Each journal lives at `terminal-drive/<agentId>/journals/<sessionId>.json` — one small JSON file per driven session, beside (but distinct from) the agent's persistent `projects/` working directory under `workspace/`.

The journal is what lets a long autonomous drive **survive a daemon restart**: a durable drive launches the driven CLI inside a detached tmux server that outlives the daemon, and the journal records the compact drive state — the objective, the last screen classification, the prompts already answered, and the steps tried — so on restart the driver **re-attaches to the live tmux session and resumes** instead of starting over or re-answering prompts. A genuinely-gone session is reported as failed with its journal preserved.

The journal is **content-free and secret-redacted** (no captured terminal output, no message bodies, no credentials — secrets are scrubbed before anything is recorded), written atomically with mode `0o600` inside a `0o700` per-agent directory. It is reaped at the session's end of life.

**Written by:** the daemon, on each update to a durable drive's state (and recovered on the next startup).
**Safe to delete?** Yes, when the daemon is stopped. Deleting a journal means an in-flight durable drive cannot resume after the next restart — the driver treats the session as gone rather than resuming it. Non-durable drives (the explicit `drive.durable: false` opt-out) never write here.

***

### `models/whisper/`

The on-disk cache for the **keyless in-process local STT engine** (whisper, run via Transformers.js / ONNX Runtime). When voice transcription resolves to the `local` provider without a `local.baseUrl` server, the engine lazy-downloads a small model (`integrations.media.transcription.local.model`, default `base`) **once** over TLS from the model hub on the first voice note, writes it here, and reuses it for the rest of the process. The directory lives under the data dir, so it is already inside the daemon's `--allow-fs-write=${COMIS_DATA_DIR}` scope — no extra permission grant is required (see [Voice](/media/voice#transcription-speech-to-text) and [Node Permissions](/reference/node-permissions#keyless-local-stt-whisper-engine)).

It holds model weights only — **no secret, no credential**, nothing user-specific.

**Written by:** the local STT adapter, on the first transcription that uses the in-process engine.
**Safe to delete?** Yes. It re-downloads on the next local transcription; deleting it only forces one slower first call afterward. Never created (or needed) when you use a `local.baseUrl` server or a keyed cloud provider instead.

### `models/tts/`

The on-disk cache for the **keyless in-process offline TTS engine** (`local`/`piper`, a single-speaker `text-to-audio` model run via Transformers.js / ONNX Runtime — the TTS twin of `models/whisper/`). When a voice reply resolves to the `local`/`piper` TTS provider, the engine lazy-downloads a small voice model **once** over TLS from the model hub, writes it here (a separate subdir from `models/whisper/`), and reuses it for the rest of the process — so synthesis needs **no network** thereafter. Already inside the daemon's `--allow-fs-write=${COMIS_DATA_DIR}` scope — no extra permission grant (see [Voice → Text-to-Speech](/media/voice#text-to-speech-tts)).

It holds model weights only — **no secret, no credential**, nothing user-specific.

**Written by:** the local TTS adapter, on the first reply that uses the in-process offline engine.
**Safe to delete?** Yes. It re-downloads on the next local synthesis; deleting it only forces one slower first call. Never created (or needed) when TTS uses `edge` or a keyed cloud provider.

Voice observability adds **no new on-disk artifact**: a transcription/synthesis records onto the **existing per-session trajectory** (the same `*.trajectory.jsonl` that carries every other turn event), so `comis explain` reconstructs the voice turn and `comis system-health` surfaces the `voice_health` signal without a dedicated voice file (see [Voice → Observability and cost](/media/voice#observability-and-cost)).

***

### `agents/`

Holds per-agent RPC state such as subagent result files written during multi-agent graph execution.

**Written by:** the daemon during subagent runs.
**Safe to delete?** Yes, between runs. Active runs may be disrupted.

***

### `background-tasks/`

Persisted state for long-running background tasks promoted out of the LLM
execution loop. Each task lives at
`background-tasks/<agentId>/<taskId>.json`; directories are owner-only (`0700`)
and files are owner-only (`0600`).

The record preserves the originating conversation, terminal result, continuation
execution id, retry count, and atomic delivery-ownership/outbox state. A
completed task can therefore resume exact-origin continuation delivery after a
daemon restart without allowing live consumption and a fallback continuation to
both own the same result.
Delivery that is proven permanent is parked as such; delivery whose outward
outcome cannot be established is parked uncertain instead of being replayed
blindly.

Admission is not reported until the protected task record has reached a
classified persistence outcome after its atomic write, rename, and applicable
sync or rollback checks. Open, write, file-sync, close, or rename failures
before the live replacement reject promotion and surface the persistence error
to the foreground turn. A non-permission-model directory-sync failure after the
rename starts neutralization: Comis writes the previously accepted record -- or
a schema-invalid tombstone for a new task -- to a synced sibling rollback file,
atomically renames it over the live record, and syncs the parent directory. A new-task
tombstone is then unlinked and the parent directory is synced again. A
successfully rejected admission therefore leaves no task that startup recovery
can adopt. If any rollback open, write, file-sync, close, rename, unlink, or
directory-sync step cannot prove that neutralization, Comis retains the live
task and reports `committed_durability_uncertain` instead of creating an
unowned recoverable execution. The one narrower exception is Node's Permission
Model reporting that its `fsync` API is disabled: after the write and rename
succeed, Comis accepts the record as committed without `fsync`. Other
persistence errors are never treated as that restriction.

**Written by:** the daemon when background tasks are created or updated.
**Safe to delete?** Only when no task is running and no completion remains
undelivered or parked for reconciliation. Deleting an active record loses the
runtime and continuation state; deleting an undelivered completed record loses
the pending continuation.

***

### `identity/device.json`

An Ed25519 key pair that uniquely identifies this Comis installation. Generated on first startup. Used for device-level signing. Stored with mode `0o600`.

**Written by:** the daemon on first startup (if it does not exist).
**Safe to delete?** Technically yes, but the device will get a new identity on next start. Any device-pairing records that reference the old key will become invalid.

***

### `devices/`

Paired-device records, written when another Comis installation pairs with this one. Each file is a small JSON record signed against the device identity above.

**Written by:** the daemon on pairing.
**Safe to delete?** Yes. The pairing is invalidated and the other side will need to re-pair.

***

### `~/.comis-chanlive/<channel>.json` *(test-harness only, outside `~/.comis/`)*

A **per-channel handle file** written by the [channel-emulation harness](/developer-guide/channel-emulation-harness) — the contributor test framework that drives Comis through the real channel adapter against an isolated, throwaway daemon. It lives in its **own** directory, `~/.comis-chanlive/` (a sibling of `~/.comis/`, **not** inside it), and is created the first time you run the harness rig (`tg up` or a `startRig` scenario). It is **not** part of a normal install — a daemon you run for real never writes it. The directory can be relocated with the `COMIS_CHANLIVE_DIR` env var (the harness points cross-process `tg` invocations at one throwaway dir this way).

The handle records the rig's three endpoints (the emulator `/control/*` base, the rig-control base, and the gateway `/rpc` + `/health` URL), the **admin-scoped gateway token**, the fixed test chat id (`424242`), and the rig's throwaway `COMIS_DATA_DIR` / `memory.db` path — so the `chan`/`tg` CLI can discover-or-spawn a running rig. For a **detached** rig (`tg up --detached`, the cold-shell path) it also records the rig subprocess's **`pid`** and a real rig-control HTTP endpoint, so a separate-shell `tg down` / `tg restart` can drive (or reap) the surviving rig.

<Warning>
  Because it carries the gateway token, the handle is written with mode `0o600` (owner-only) so the admin token cannot leak off-box. The token is a real admin-scoped credential for the throwaway rig's loopback gateway — treat the file like any other secret-bearing file. If you see a stale `~/.comis-chanlive/` after a harness run, it is safe to delete when no rig is running.
</Warning>

**Written by:** the channel-emulation harness rig (`test/live/`), on `tg up` / `startRig`.
**Safe to delete?** Yes, when no harness rig is running. The next `tg up` re-creates it.

***

### `dead-letters.jsonl`

Append-only JSONL of announcement-delivery failures that exhausted in-process retries (channel down, bot blocked, etc.). The delivery system replays these on provider recovery and expires entries after one hour. Only created when at least one announcement has been dead-lettered.

**Written by:** the announcement delivery subsystem.
**Safe to delete?** Yes. Pending re-deliveries are dropped; entries auto-expire after one hour anyway.

***

### `.daemon.lock`

The daemon acquires this exclusive lock (`O_EXCL`) at startup to prevent two daemon processes from running against the same data directory simultaneously. If the file is present when the daemon starts, it attempts stale-lock recovery (opens the file, checks whether the recorded PID is still running, and removes the stale lock if not).

**Written by:** the daemon at startup.
**Safe to delete?** Yes — but ONLY when you are certain the daemon is stopped. Deleting this file while the daemon is running will break the lock invariant and may allow a second daemon instance to start.

To check if the lock is stale:

```bash theme={}
cat ~/.comis/.daemon.lock    # shows the PID that holds the lock
ps -p <PID>                  # check if that PID is running
```

***

### `auth-profiles.json` *(file mode only)*

Stores OAuth profiles (access and refresh tokens) when `security.storage: file` is configured. Each profile is a JSON entry identified by provider and email. The file is written atomically (tmp then rename) with mode `0o600`.

**Written by:** `comis auth login` and the daemon's OAuth refresh path.
**Safe to delete?** Deleting this file removes all stored OAuth credentials — you will need to re-run `comis auth login` for each provider. Do not delete while the daemon is running.
**Not present when:** `security.storage: encrypted` (the default) — OAuth profiles are stored in the `oauth_profiles` table inside `secrets.db` instead.

***

### `mcp-tokens/` *(file mode only)*

A directory holding per-MCP-server OAuth token JSON files when `security.storage: file` is configured. Each file is named after the MCP server identifier. These are the access tokens issued during `comis mcp login`.

**Written by:** `comis mcp login` and the daemon's MCP OAuth refresh path.
**Safe to delete?** Deleting the directory (or individual files) clears stored MCP OAuth tokens for those servers — you will need to re-run `comis mcp login`. Do not delete while the daemon is running.
**Not present when:** `security.storage: encrypted` — MCP tokens are stored in the `mcp_credentials` table inside `secrets.db` instead. Also not present in `security.storage: env` mode, where MCP OAuth login is unavailable (there is no writable MCP token store — `comis mcp login` fails with an actionable storage-mode error rather than writing a plaintext file).

***

### `secrets.json` *(file mode only)*

The plaintext secret store when `security.storage: file` is configured. Contains named secret key-value pairs (for example, `ANTHROPIC_API_KEY`). Mode `0o600`. Written atomically.

**Written by:** `comis secrets set` and the daemon's secrets RPC path.
**Safe to delete?** Deleting this file removes all stored named secrets — every agent that depends on those secrets will fail to make API calls until the secrets are re-added. Do not delete while the daemon is running.
**Not present when:** `security.storage: encrypted` (the default) — named secrets are stored in the `secrets` table inside `secrets.db` instead.

***

### `broker.sock`

A Unix domain socket created by the credential broker at daemon startup when the broker is active. Agents connect to this socket to get ephemeral single-use bearer tokens injected into their outbound HTTP requests, so the raw API key never enters the agent namespace.

**Written by:** the daemon at startup (when `executor.broker` is configured).
**Safe to delete?** The socket is ephemeral — deleted and recreated on each daemon start. If you see a stale `broker.sock` after a crash, you can delete it safely before restarting.
**Not present when:** `executor.broker` is not configured or the broker is disabled.

***

### `cap.sock`

A Unix domain socket (`0600`, owner-only) created for the **loopback capability endpoint** when an autonomy-bearing agent is configured. The jailed script surface dials this socket, presents its [`COMIS_CAP_LEASE`](/reference/environment-variables#comis_cap_lease) bearer, and the endpoint validates the lease (timing-safe, not-expired, not-revoked, audience-bound) and dispatches through the same handler gates as every other origin. The lease the endpoint validates is held **in memory** (run-scoped) — there is no lease table on disk, so this socket is the only on-disk artifact of the capability layer.

**Written by:** the daemon at startup (when an agent resolves to an autonomy-bearing profile).
**Safe to delete?** The socket is ephemeral — `0600`, deleted and recreated on each daemon start. A stale `cap.sock` after a crash is safe to delete before restarting.
**Not present when:** no agent resolves to an autonomy-bearing profile.

## Backing up

To migrate to a new machine, back up these items:

```bash theme={}
tar -czf comis-backup.tar.gz \
  ~/.comis/config.yaml \
  ~/.comis/.env \
  ~/.comis/memory.db \
  ~/.comis/secrets.db \
  ~/.comis/workspace
```

`config.last-good.yaml`, `logs/`, `traces/`, and `ecosystem.config.js` are machine-specific or regenerated and do not need to be included.

<Note>
  If you use `secrets.db`, make sure you also have your `SECRETS_MASTER_KEY` stored somewhere safe. Without it the database is unreadable, even with the file itself.
</Note>

## Custom location

By default Comis resolves config from `~/.comis/config.yaml` and `~/.comis/config.local.yaml`. You can override this with `COMIS_CONFIG_PATHS`, a colon-separated list of absolute paths:

```bash theme={}
COMIS_CONFIG_PATHS="/etc/comis/config.yaml:/home/me/.comis/config.yaml" node packages/daemon/dist/daemon.js
```

The data directory (`~/.comis/`) is separately controlled by `COMIS_DATA_DIR`. See [Configuration](/installation/configuration) for a full reference.

## Permissions

Comis enforces strict file modes on startup. If any of the following are wrong, the daemon corrects them automatically and logs what it changed:

| Path                    | Required mode                |
| ----------------------- | ---------------------------- |
| `~/.comis/` (directory) | `0o700` (owner-only)         |
| `config.yaml`           | `0o600`                      |
| `config.local.yaml`     | `0o600`                      |
| `.env`                  | `0o600`                      |
| `secrets.db`            | `0o600`                      |
| `ecosystem.config.js`   | `0o600` (set by `pm2 setup`) |
| `identity/device.json`  | `0o600`                      |

<Warning>
  Do not run the daemon as root. The `0o700` directory mode means only your user can read or write the data directory.
</Warning>

## Resetting

To start completely fresh:

```bash theme={}
# Stop the daemon first
pm2 stop comis   # or pkill -f daemon.js

# Remove everything
rm -rf ~/.comis

# Re-run setup
node packages/cli/dist/cli.js init
```

To reset only conversation history while keeping your config and credentials:

```bash theme={}
rm ~/.comis/memory.db
# The daemon recreates an empty database on next start
```

To reset logs only:

```bash theme={}
rm ~/.comis/logs/daemon.log*
```
