Skip to content

Data import & export

What this is for. Data port is the import wizard. It scans a server path or an uploaded zip/markdown from another assistant (Claude Code, Grok CLI, Cursor, Codex CLI, Gemini CLI, Windsurf, GitHub Copilot, Obsidian, a chat export, a prior EYAS export, or a plain Markdown folder) and proposes what to file where. Memory can apply; workspace rules and identity are proposal only until you approve a merge. It is not a full-DB dump — use Backup for recovery. Export is still Coming soon.

Location: Settings → Data portability card. Heading: Import memory, skills and rules from previous AI systems. Export is coming later.

  • You are moving durable notes out of ~/.claude or an Obsidian ai-memory vault into EYAS (the only memory that later turns will read).
  • Custom skills live in Claude/Cursor and should become own skills here.
  • Agent workspace rules/identity should be reviewed as merge proposals, never auto-overwritten.
  • You want a zip of a prior export scanned without copying files onto the server by hand.
  • You used to reach another tool’s memory live — a host CLAUDE.md loaded by Claude Code, an import root inside ~/.claude, a Memory / Qdrant / Obsidian MCP server — and EYAS now refuses it. The import is the sanctioned way in: one-way, once, with the origin recorded.
  1. Open Settings → Data portability → Import data…
  2. Pick Source system: Auto-detect, Claude Code, Grok CLI, Cursor, Codex CLI, Gemini CLI, Windsurf / Codeium, GitHub Copilot, Obsidian, Chat export, EYAS export, Markdown folder. The ids the API uses are claude-code, grok-cli, cursor, codex, gemini-cli, windsurf, copilot, obsidian, chat-export, eyas-export and generic-md.
  3. Give a Server path (absolute on this machine) or Choose file… (zip or a single markdown/JSON). Optional Instructions guide ranking.
  4. Scan. Review the folder tree and the groups (Memory, Memory index, Sessions, Skills, Rules, Identity, Agent personas, Knowledge, Source code, Not importable). Select what to keep. Leave Enrich metadata with the model unchecked for a fully deterministic import.
  5. Import N items. Memory/skills apply; rules/identity wait as Workspace change proposals — Approve merge or Reject.
CapabilityMeaning
ImportPath on server and/or upload (zip)
TargetsMemory (kind + tier), episodic sessions, skills with bundled files, agent personas, workspace rules, project-type prompt
MergeProposal only for rules/identity — apply after explicit approve
EnrichmentOff by default — an opt-in checkbox, metadata only (kind, summary, tags, checked links), never the body; enriched notes say who answered (enriched_by)
LanguageImported memory keeps source language
Skills categoryImported → own
RollbackUndo a whole job — needs the delete permission on Data Port
ExportComing soon — an eyas-export-v1 bundle (vault, skills, workspaces, episodic.jsonl). A bundle can carry credential-bearing notes, so before it ships the route gets the same owner-only caller test that guards memory recall; create on Data Port alone will not be enough

You do not have to pick the perfect folder. Everything under the path is mapped. There is no keep-list and no cap: the scan walks every directory below the root, whatever its size. A tree ten times larger than a typical home directory is listed and imported in full — the cost is time and disk, never omission. Only directory classes that can never hold memory are not entered: dependency folders (node_modules), version-control folders (.git, .hg, .svn), .cache, __pycache__, .venv / venv, build outputs (dist, build, out, .next, .turbo, target) sitting beside a build manifest, browser profile roots (Chrome, Chromium, Firefox, Antigravity — recognised by their marker files, wherever they sit), cloud storage roots (Library/CloudStorage and Library/Mobile Documents, recognised by the place macOS gives them, plus legacy sync folders such as Dropbox recognised by their own marker files — a folder merely named OneDrive or Dropbox is an ordinary folder and is walked), the trash (.Trash, .Trashes, $RECYCLE.BIN, .local/share/Trash) and Library/Caches. Each of those is still one visible row carrying its file count and the reason Folder not searched: <class>, so nothing disappears in silence. A symlinked directory is followed once — the real path decides — and a loop is reported rather than re-entered. .DS_Store is listed as application state.

SourceEYAS layer
Note with type: user / feedback / project / reference (Claude Code, Obsidian, Grok)Vault note with that kind; feedback under procedural/, the rest under semantic/; the file keeps its source file name, so [[wikilinks]] keep resolving
MEMORY.md indexOne vault note tagged index; a one-line hook becomes the summary of the note it points to when that note declares no description of its own
Session summaries, session notes (type: claude-session / grok-session) and transcripts — Claude Code *.jsonl including sub-agent transcripts, Cursor agent transcripts, Codex rollouts, ChatGPT / Claude.ai exportsEpisodic memory, one row per session — a very long session as ordered parts, never cut — with the turns kept verbatim. All of it is selected by default; untick the Sessions group if you do not want it. Tool output saved beside a session is listed but not ticked
Legacy memory folders (memory.local-backup-*, memory.old, *.bak)Vault note tagged legacy; a name already taken gets a -2 sibling instead of being dropped
Your own documents, anywhere under the rootVault note; the kind comes from type: when the note declares one, otherwise reference
Third-party product documentationVault note tagged third-party, selected — untick the group if you would rather not keep it
Rule files inside repositories (.cursor/rules/*.mdc, .cursorrules, AGENTS.md, CLAUDE.md, GEMINI.md, global_rules.md)Proposal, wherever in the tree they sit
Source-code filesListed and importable, not ticked — they are neither memory nor instructions
Data and configuration text (.yaml, .toml, .csv, .log, an assistant’s settings.json …)Listed and importable, not ticked
SKILL.md with references/ and scripts/One own skill: the whole package verbatim, files also copied to data/skills/imported/<name>-<hash>/. The skill body names that directory by its absolute on-disk path, so a bundled script can be run straight from there
CLAUDE.md, AGENTS.md, GEMINI.md, Cursor .mdc, Windsurf and Copilot rulesProposal for the primary assistant’s AGENTS.md — or, when you choose Project-type prompt, for the general project type — appended on approval, never overwritten
.claude/agents/*.md personasAgent definitions (tools mapped to EYAS tools)

Nothing is clipped or silently skipped. Files are read whole at import time and there is no size limit: a text file over 4 MiB (50 MiB for a chat export) is imported in full and only carries a size marker on its row. Bodies are written byte for byte, leading and trailing blank lines included; the vault writer’s single trailing newline and a dropped UTF-8 byte-order mark are the only changes. Every applied item records the adapter that actually read it (source.adapter, and the tag source:<adapter> — a Grok summary found under an auto-detected Claude Code job is tagged grok-cli), every path the content was found at, and the sha256 of its content in the ledger — for vault notes, episodic rows, skills, bundled skill files, agents and proposals alike. The original frontmatter, path, hash and modification time travel with the note under source:. Every file the scan does not import is a visible row with its reason. Re-running an import reports Unchanged for notes already present and never overwrites — a different note with the same name gets a -2 suffix and a conflict-with: tag. Re-importing an unapproved rule file does not stack up a second proposal for it.

A declared project or project type that doesn’t exist yet isn’t lost. When an imported note’s frontmatter declares a project (or projectType), the importer files it under projects/<id>/ (or project-types/<id>/) only if that project or project type already exists in this EYAS instance; otherwise it lands unscoped and tagged declared-project:<id> (or declared-project-type:<id>). Creating the project afterwards and re-importing then files it under the declared id, or you can move the note yourself.

Any assistant. Sources are recognised by adapters: Claude Code, Grok CLI, Cursor, Codex CLI, Gemini CLI, Windsurf, GitHub Copilot (documented layout only), Obsidian, ChatGPT / Claude.ai / generic JSON exports, eyas-export and plain markdown (generic-md). Files reached through several paths (a vault symlinked into ~/.grok/memory) become one note.

Model enrichment is off by default. The review step carries an Enrich metadata with the model checkbox. Left unchecked — the default — the whole import is deterministic and spends no model call at all; nothing changes for such imports. Ticked, notes without a declared type get a suggested kind, summary, tags and links from EYAS’s background model: the Heartbeat routing tier first, then the install default, then API providers, then CLIs that can run isolated. There is no item cap; the cost is time. The body is never rewritten, nothing is skipped on the model’s say-so, and an item tagged contains-secrets is never sent. The result panel reports how many notes were enriched.

  • Only an isolated answer is used. An answer counts only when it comes from the background model through an isolated call (one turn, no tools): an API provider, or a CLI that can run isolated. If no such model is available (for example a Grok-only or Kimi-only install), if the call fails or comes back empty, or if the model refuses, the note keeps exactly its deterministic metadata and is counted as a fallback in the result panel.
  • Links are checked. A link the model suggests is kept only when it names a note that already exists in the vault (by file name or title) or a note selected in the same import, and it is written as that note’s file name. Every other link the model proposes is dropped. Links the source file itself contains are always kept unchanged.
  • Enriched notes say who answered. Every enriched note carries an enriched_by field in its frontmatter, for example enriched_by: {provider: anthropic, model: claude-haiku-4-5, route: tier}. provider is the provider that answered; model is the model it reported, omitted when the backend named none (a CLI used with its own default); route is how the background model was chosen — tier (the routing tier, Heartbeat first), default (the install default), api (an API provider) or isolated-cli (a CLI that can run isolated). The field stays when the note is later rewritten. Notes imported without enrichment have no enriched_by.
  • The background model is looked up when a job runs, not when the server starts, so enrichment works even when the model module became available after the Data Port.

Cleaning an earlier enriched import. Imports enriched before this update carry no enriched_by and may hold links that were never checked. To redo one without enrichment: open the job in the result panel or in Previous imports and choose Roll back this import, then scan the same source again and import with Enrich metadata with the model unticked. Notes whose body you edited since are kept and reported as skipped.

Rolling back. Every job can be reverted from the result panel or the Previous imports list: notes, episodic rows, skills (and their copied files), agents and approved rule sections are removed; pending proposals are rejected. Rollback is destructive, so it needs the delete permission on Data Port — owner and admin only under the shipped defaults; everyone else sees the button fail with a permission error. A vault note you have edited since the import is left alone and reported under skipped instead of being deleted.

The importer reads other tools’ stores itself, not through a model tool, so it is unaffected by the security gate’s memory policy — which refuses every model reads and writes of ~/.claude, ~/.grok, ai-memory folders, Obsidian vaults and the rest (see Security & privacy — Memory outside EYAS). An agent that used to read such a file directly now gets a refusal; import the content once here instead. See Memory.

This is the way in for content EYAS no longer reads live:

  • Host CLAUDE.md and host skills. Claude Code always runs isolated and no longer loads them; import them once here. See Providers — Claude Code isolation.
  • Import roots inside another tool’s folders. skills.importRoots / agent.importRoots entries inside ~/.claude, ~/.grok, ~/.agents, an Obsidian vault and similar are skipped at start; import that folder once here, then remove the entry from local.yaml. See Configuration.
  • Memory-store MCP servers. The Memory, Qdrant, Obsidian and MCPVault MCP servers, and any server pointed at a vault or at another tool’s memory, are blocked for every model; copy that memory into EYAS here. See MCP.
  • Deleted imported agents. An agent imported from agent.importRoots that you deleted is not created again at start; import its file here to get it back.

A file the secrets heuristic flags is imported verbatim, like every other file, and tagged contains-secrets. Nothing is ever dropped for holding a credential — safety here is a recall measure, not an import measure. The tag travels as a note tag, a skill capability, an episodic tag and [contains-secrets] in a proposal title, and the wizard marks the row so you can see it before you import.

The heuristic looks for a private-key block, a provider token, a KEY=value literal or a .env-shaped file name. Code that looks a secret up — keychain_lookup(...), os.environ[...], getenv(...) — and documentation placeholders such as <your-key>, xxx or .env.example are not flagged.

What the tag does. A note, episodic row or skill tagged contains-secrets is left out of everything the model reaches on its own: the standing memory index, recall, search_memory, the reflection job, the nightly consolidator, the skill matcher and the assembled system prompt. It is never handed to the optional enrichment model and it is never embedded. The tag also carries over to what EYAS derives from the item — its copy in the raw record, the facts extracted from it and the summaries built from it — so none of those reach the model either (see Memory — Imported secrets stay out of recall). It stays fully visible to you on the Memory page. To open it to the model, set memory.recall.includeSecrets: true in config/local.yaml and restart — see Configuration.

This is an automatic-inclusion gate, not a filesystem sandbox. The tag stops a flagged item being pulled into a prompt by itself. It does not stop an agent that holds file-read tools from reading the original file on disk, and it encrypts nothing. If a credential should not be on this machine at all, rotate it: the importer’s job is to keep it out of a prompt you did not ask for, not to make it unreachable.

Two kinds are not gated, because there the content is the prompt. An imported agent persona and an approved workspace rule file are used verbatim in the assistant’s prompts, so a credential inside one reaches the model on every turn and recall filtering does not apply — gating them would disable the very agent you imported. They are still tagged contains-secrets so you can find them, and the wizard says the same thing on those rows: review them before importing.

Credential-shaped files — .env, credentials.json, key files — are listed and importable but not ticked. A note, rule, skill or transcript that merely contains a key keeps its own kind, stays ticked, and carries the tag.

Every candidate is one row in the database, so neither the wizard nor the server ever holds the whole list.

The scan runs in the background. It answers straight away and the folder tree fills in while it walks, reporting folders visited, files seen and rows listed. A whole home directory takes minutes. You can stop it, and what it mapped before you did stays reviewable.

The review step is a folder tree, a virtualised list and a preview. Every folder is mapped, the classes that are not entered included — those show their file count and the class that kept the walker out. Select all importable, Select none and Back to suggested act on the whole scan. Below them, selection is by gesture rather than row by row: every folder and every kind carries a tri-state checkbox, so one click unticks every transcript in every folder. The number beside each checkbox is the server’s own answer for the selection on screen, so what you read is what the import will file. The preview shows the first 64 KiB of a file; the import still takes it whole.

The import streams. Items go in batches of 100, each batch is committed, progress is reported every 100 items, and the search index is rebuilt once at the end. You can stop it after the current batch — everything already filed stays and can be rolled back. If the server restarts mid-import, the job resumes from its last committed batch instead of starting over. Scan time and import time are both shown.

One large file is the memory bound, not the whole tree. Importing a container — a transcript, a chat export, a Codex database — costs several times its own file size in transient memory, because the file is held as one buffer while every unit inside it is rendered. How many times depends on what the file is: about three times for a line-per-turn transcript, and seven or more for a chat export, which is parsed whole into one object graph. The import phase costs more than the scan — a 19 MB transcript measured 100 MiB while scanning and 235 MiB while importing. A 90 MB chat export peaks near 900 MiB resident, which the Helm chart’s 1Gi default has room for and the 512Mi legacy starter does not.

Row-by-row memory stays flat, however many rows there are. Nothing is kept once a scan finishes: a second scan of the same 26 000-row tree adds nothing at all to the heap. Resident memory can still look high afterwards, because the allocator holds on to pages it has already asked the system for — that is the allocator, not the scan. The bound is set by your single largest file, not by the size of the tree.

Re-running adds only what is new. Everything already present reports Unchanged, and a later import never rolls back an earlier one.

Two engine facts, stated so that nothing surprises you:

  • A bundled skill file over 200 000 characters is inlined into the skill body up to that point, with a marker naming the complete copy. The copy under the skill’s asset directory is byte-exact and complete.
  • A single text file larger than one text value the engine can hold (about 512 MiB) is listed, hashed and selectable like any other, but reported with the reason Larger than one text value the engine can hold instead of being filed. The row says why.

One caveat about identity. Sessions and skills have no path identity — they are recognised by the digest of their content. A whitespace-only edit to a source file after an import therefore produces a second episodic row (and a second skill, when the package bundles files) rather than updating the first. Vault notes, which do have path identity, are re-stamped in place.

Import wizard

Steps: source → scanning → review → running → done.

ControlMeaning
Source systemProfile listed above
Server pathAbsolute path — a folder or a whole home directory. Choosing a Source system other than Auto-detect lists Typical locations for it, straight from that adapter
Upload archive or fileZIP of a prior export, or a single markdown/JSON file. The 50 MiB body limit is the upload’s only; a path scan has none
InstructionsOptional — what to look for. It guides ranking only; nothing is dropped because of it
ScanMap the tree in the background — folders visited, files seen, rows listed, and Stop the scan
Folders found by the scanEvery folder the scan mapped, with per-folder toggles, subtree counts and Reasons the rows in this folder carry — importable or not, including the class of a folder that was not entered
Kind filterAll / Memory / Memory index / Sessions / Skills / Rules / Identity / Agent personas / Knowledge / Source code / Not importable
Select all importable / Select none / Back to suggestedBulk selection over the whole scan, not just the page
Folder and kind checkboxesTri-state — one click selects or clears everything importable under a folder, or every row of a kind in every folder
PreviewThe first 64 KiB of the file — the import still takes it whole
Enrich metadata with the modelOff by default — opt-in, metadata only (kind, summary, tags, links to existing notes), never the body, never a flagged item; used only from an isolated background-model answer, otherwise counted as a fallback
Import N itemsStart the background job
Stop this importStops after the current batch; what is filed stays
StatsSelected / Applied / Unchanged / Proposals / Skipped / Errors
Skipped, by reasonOutcome counts per reason code (see below)
Scanned in / Imported inHow long each phase took
Approve merge / RejectWorkspace proposals — never auto-merged
Roll back this importUndo the whole job — needs delete on Data Port
Previous importsThe last five jobs, each with its own rollback button

Empty scan: Nothing importable found in this location.

Every row in the review list carries a reason, and every outcome that is not a clean apply is counted under Skipped, by reason. Both come from one fixed vocabulary — never free text — so the wizard can show the label below while the API and the logs carry the code.

CodeWhat it means
directory-skippedFolder class never entered — one counted row, with its file count and the class
binaryBinary file
outside-rootOutside the chosen folder
duplicate-contentIdentical content
unreadableCould not be read
emptyEmpty file
derived-indexGenerated index
transcriptConversation transcript (imported whole)
session-summarySession summary
session-artifactTool output saved with a session
personaAgent persona
slash-commandSlash command
cursor-ruleCursor rule file
memory-noteMemory note
memory-indexMemory index
skill-packageSkill package
skillSkill file
orphan-assetBundled file of a skill that was not imported
rules-fileRules file
configConfiguration file
source-codeSource code file — importable, not ticked
data-fileData or configuration text — importable, not ticked
symlink-uploadSymlink in the upload
needs-bunNeeds the Bun runtime
not-downloadedStored in the cloud, not downloaded — listed from what the file system knows, never fetched
invalid-jsonInvalid JSON
unknown-jsonUnrecognised JSON
unrecognisedNot recognised
app-stateApplication state
identityIdentity file
not-durableThird-party or boilerplate text — label only
tools-policyTool policy
not-importableNothing importable in it
missing-unitIts part of the file was gone
unsupported-targetDestination not supported
service-unavailableThe service was unavailable
not-a-personaNot an agent persona
no-agentNo agent to receive it
exceeds-string-limitLarger than one text value the engine can hold — listed, not filed yet
unchangedAlready imported, unchanged
errorFailed with an error

not-durable and transcript are labels only: neither unselects anything. The Unknown kind is not produced any more — it survives only on scans made before this release.