AI Coding Assistant Context Migration: Export, Convert, and Verify Instructions and Memory Assets
This article explains how to treat an AI coding assistant's instruction files, rules, and long-term memory as portable engineering assets. It models context in four layers, shows why point-to-point migration between tools usually fails, and proposes a five-stage pipeline built around a tool-agnostic intermediate representation (IR). You will find concrete implementations for inventory and snapshots, parsing and normalization, deduplication and conflict handling, path rewriting, capability mapping, memory governance, and rendering, plus a three-part verification approach and a long-term governance model based on version control and periodic migration drills.
1. Why Bother Migrating: From “Account Limits” to Portable Context Assets
Over the past few years, AI coding assistants have become a fixed part of many teams’ development workflows. At the same time, community feedback has reported shifts in account and access policies: according to community reports and public discussion (details should be taken from official statements; this article does not cite unverified figures or individual cases), some developers run into situations that require reconfiguration when they change environments or tools.
To be fair, collecting device and network characteristics for anti-abuse purposes is not uncommon in cloud services and SaaS products, and this kind of risk control often offers ordinary users little detail and no appeal path. But the point of this article is not the account itself, but a more controllable engineering problem: when an account, a policy, or a tool version changes, what is genuinely hard to rebuild is not the account — it is the instructions, rules, and long-term memory accumulated in configuration directories, which encode a team’s long-term experience. Those assets cannot be bought elsewhere, and they cannot be copied in one click.
Compliance note: This article describes the engineering method for moving a developer’s own configuration from one environment to another. The only migration targets are context files the user legitimately owns. It is not intended for account sharing, resale, bypassing platform risk controls, or violating any terms of service.
So the question worth more attention than the account is: how do we make these context assets exportable, convertible, verifiable, and rollback-ready? Starting from an asset inventory, this article lays out a migration pipeline that can actually be put into practice.
2. Problem Modeling: What Counts as a “Context Asset”?
To get an AI coding assistant to understand an unfamiliar project, three kinds of information have to be supplied continuously: who you are, what the project is, and what happened before. On a developer’s machine, this roughly splits into four layers:
| Layer | Typical content | Common form | Migration difficulty |
|---|---|---|---|
| Instruction | Role definition, answering style, global and project-level conventions | Markdown with metadata headers | Low |
| Rule | Tech stack constraints, code standards, commands and skills | Structured directories, command templates, hooks | Medium |
| Memory | Project background, long-term preferences, corrected mistakes, key decisions | JSON, JSONL, database, or vector index | High |
| Runtime (implicit assets) | Local service addresses, connection strings, test commands, IDE configuration | Environment variables, shell history, config files | Depends |
The migration difficulty differs per layer. The instruction layer is essentially a system prompt text; if the target tool supports a custom instruction file, it can be carried over almost as-is — but the effective scope, load order, and conflict precedence are not the same across tools. The rule layer is tightly coupled to the tool’s runtime conventions, so a straight copy often breaks. The memory layer is the most underestimated one: its format, trigger conditions, and injection point all differ, so a simple copy almost certainly fails, and asking a prompt to “reproduce it verbatim” is limited by output length, context window, and paraphrase accuracy. The runtime layer is not strictly context, yet it determines whether migrated rules can actually take effect.
A common mistake is to equate “migration” with copying files. Files can be copied; semantics cannot necessarily. The migration goal should therefore be defined as: loading the original information in the new environment in an equal or better way, with results equivalent to or better than before the migration.
3. Why Migrations Fail: Four Frequent Traps and How to Avoid Them
In practice, migration failures are rarely caused by a tool that cannot import anything. They come from a missing engineered middle layer. Common causes, with mitigations:
- The native import path is not enough. Some tools’ import entries only cover explicit assets such as chat history or plugins, and ignore long-term memory and custom rules; the import action itself may trigger additional requests. Mitigation: do not rely on a single entry point — use a unified intermediate layer and convert offline.
- Path and format incompatibility cause parse errors. One side is Markdown with metadata headers, the other is JSON entries; pointing both at the same directory results in read failures or a large number of malformed entries. Mitigation: normalize first, then render according to the target’s conventions.
- Implicit dependencies are hard to discover. Instructions may reference built-in capabilities, command prefixes, or hook events that only exist in the original tool, and the problem only surfaces at runtime in the new environment. Mitigation: use a capability mapping table to declare these explicitly and mark them “needs manual confirmation.”
- Manual copying causes information loss. Copying entry by entry inevitably drops or corrupts items, and there is no way to verify completeness. Mitigation: migrate with a script, and back it up with entry counts and hash verification.
Conclusion: do not convert point-to-point between two tools. Introduce a tool-agnostic intermediate representation and separate the three phases of parse, process, and output.
4. Solution Overview: A Migration Pipeline Built on a Unified Intermediate Representation
The whole approach can be abstracted as a five-stage pipeline:
Source tool config dir Unified IR Target tool config dir┌──────────────┐ parse ┌──────────────────┐ render ┌──────────────┐│ instruction │ ─────────> │ │ ────────> │ instruction ││ rule/command │ │ entry: id/kind/ │ │ rule/command ││ memory store │ │ body/meta/hash │ │ memory store │└──────────────┘ └──────────────────┘ └──────────────┘│ │dedupe · conflict handling verify · dry run · rollbackpath placeholder rewriting ──── capability map ───> target-side tool binding
Each entry in the intermediate representation carries at least the following fields:
{"id": "global:rule:python-style","kind": "rule","scope": "global","name": "Python code style","tags": ["backend", "style"],"content": "Use 4-space indentation consistently; public functions must have type annotations and a docstring.","priority": 100,"origin": { "tool": "source-agent", "path": "rules/python-style.md" },"hash": "sha256:9f2c…","created_at": "2024-09-12T10:21:03+08:00"}
kind decides where the entry is written, scope decides load order, priority resolves conflicts, origin keeps things traceable, hash supports deduplication and verification, and created_at supports age-based ordering.
5. Detailed Design of the Key Stages
5.1 Inventory and Snapshot: Know What You Have Before Migrating
The first step of a migration is not conversion, it is inventory. The following commands generate the skeleton of a risk report quickly:
# Snapshot (timestamped; cheap, but the most important rollback baseline)ts=$(date +%Y%m%d-%H%M%S)tar czf "agent-config-$ts.tgz" -C "$HOME" .agent-configecho "snapshot: agent-config-$ts.tgz" > snapshot.log# Inventory: count entries and bytes, locate large files and stale entriesfind "$HOME/.agent-config" -type f -printf '%TY-%Tm-%Td %10s %p\n' | sort > inventory-$ts.txt
The risk report template should include: total number of entries, total bytes, the top-N largest memory files, entries not updated for more than 180 days, and a list of entries suspected of containing keys or tokens. Write the inventory result to report.md first, then move on to parsing.
5.2 Parsing and Normalization: Collapse Different Formats into Uniform Entries
The key change: fill in the created_at field. Read source metadata first; if it is missing, set it to None and flag it in the report (it is used later by the age-based ordering in 5.6).
import hashlib, json, refrom pathlib import PathFM_PATTERN = re.compile(r"\A---\s*\n(.*?)\n---\s*\n?", re.DOTALL)def split_front_matter(text: str):match = FM_PATTERN.match(text)if not match:return {}, textmeta = {}for line in match.group(1).splitlines():key, sep, value = line.partition(":")if sep:meta[key.strip()] = value.strip()return meta, text[match.end():]def load_markdown(path: Path, scope: str, kind: str):meta, body = split_front_matter(path.read_text(encoding="utf-8"))return {"id": meta.get("id", f"{scope}:{kind}:{path.stem}"),"kind": meta.get("type", kind),"scope": scope,"name": meta.get("name", path.stem),"tags": [t.strip() for t in meta.get("tags", "").split(",") if t.strip()],"content": body.strip(),"priority": int(meta.get("priority", 100)),"origin": {"tool": "source-agent", "path": str(path)},# Prefer source metadata; record None when missing and flag it in the report"created_at": meta.get("created_at") or meta.get("date") or None,}def load_memory_jsonl(path: Path, scope: str):items = []for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):if not line.strip():continuerecord = json.loads(line)items.append({"id": record.get("id", f"{scope}:memory:{path.stem}:{lineno}"),"kind": record.get("type", "memory"),"scope": scope,"name": record.get("title", f"Memory entry {lineno}"),"tags": record.get("tags", []),"content": record["content"].strip(),"priority": int(record.get("priority", 100)),"origin": {"tool": "source-agent", "path": f"{path}:{lineno}"},"created_at": record.get("created_at") or record.get("updated_at") or None,})return items
The hash is computed from kind/name/content and deliberately excludes created_at, so that differing timestamps do not affect deduplication.
5.3 Deduplication and Conflict Handling: Where Migration Quality Is Decided
- Exact deduplication: for identical hashes, keep only the single most complete entry.
- Near deduplication: use Jaccard similarity with a recommended threshold of
0.8:
def jaccard(a: set, b: set) -> float:if not a or not b:return 0.0return len(a & b) / len(a | b)SIM_THRESHOLD = 0.8 # same tags + overlapping keywords + one text containing the other → near duplicate
- Conflict handling is not merged automatically. Emit a pending-confirmation list for human decisions. CSV template:
conflict_id,scope_a,id_a,scope_b,id_b,reason,recommend,confirmed_by,decisionC001,global,rule:api-envelope,project,rule:raw-response,project scope conflicts with global convention,keep the project scope,,
Manual confirmation fields include confirmed_by, decision (keep_a/keep_b/merge/drop), and note. Guiding principle: project scope usually overrides global scope, but team conventions win.
5.4 Path Rewriting: Pull Environment-Specific Details Out
Rewrite machine-local absolute paths into placeholders, fix the Windows regex (to avoid producing double backslashes), and add a reverse check:
PATH_RULES = [(re.compile(r"/Users/[^/]+/"), "${HOME}/"),(re.compile(r"/home/[^/]+/"), "${HOME}/"),(re.compile(r"[A-Za-z]:\\Users\\[^\\]+\\", re.IGNORECASE), r"%USERPROFILE%\\"),]def rewrite_paths(text: str) -> str:for pattern, repl in PATH_RULES:text = pattern.sub(repl, text)return textABS_PATH_PATTERN = re.compile(r"(?:[A-Za-z]:\\|/Users/|/home/)")def assert_no_absolute_paths(text: str, origin: str) -> None:offenders = ABS_PATH_PATTERN.findall(text)if offenders:# Reverse check failed: abort the migration and roll back to the snapshot# instead of continuing with a bad entryraise ValueError(f"Entry {origin} still contains absolute paths: {offenders[:3]}…; aborted, fix the rules and retry")
5.5 Capability Mapping: Decouple Instructions from the Concrete Tool
Maintain a capability mapping table that separates “capability semantics” from “tool implementation”. Capabilities the target tool does not have should be marked “needs manual confirmation” rather than silently dropped:
code_search:source: "Search capability: locate code positions by keyword or regular expression"target: "Prefer the built-in code search; fall back to terminal commands when unavailable"sub_task:source: "Task decomposition: start a separate execution context for an independent subtask"target: "When no equivalent capability exists, run the steps sequentially in the same session and summarize; mark as needing manual confirmation"
5.6 Memory Governance: From “Inject Everything” to “Retrieve on Demand”
Redact first, then retrieve on demand. Fix in the original logic: when no keyword hits are found, do not fall back to all entries. First do a local keyword match, and only treat it as empty if that still yields nothing. This also relies on export_path (generated during export in 5.7):
import os, subprocessfrom datetime import datetime, timezoneSECRET_PATTERN = re.compile(r"(sk-[A-Za-z0-9_-]{16,}"r"|(?:password|passwd|token|api[_-]?key)\s*[:=]\s*\S+)",re.IGNORECASE,)def mask_secrets(text: str) -> str:return SECRET_PATTERN.sub("[REDACTED: inject via environment variables instead]", text)def score(item: dict, now: datetime) -> float:created = item.get("created_at")if not created:return float(item["priority"]) # fall back to priority when the timestamp is missingage_days = max((now - datetime.fromisoformat(created)).days, 0)return item["priority"] * (0.5 ** (age_days / 180))def keyword_search(items, keyword, top_k=5):if not keyword:return []try:result = subprocess.run(["mdfind", "-onlyin", os.path.expanduser("~/agent-context"), keyword],capture_output=True, text=True, timeout=10,)if result.returncode != 0:raise OSError(result.stderr.strip())paths = {l.strip() for l in result.stdout.splitlines() if l.strip()}return [i for i in items if i["origin"].get("export_path") in paths][:top_k]except (OSError, FileNotFoundError, subprocess.TimeoutExpired):low = keyword.lower()return [i for i in items if low in i["content"].lower()][:top_k]def retrieve(items, keyword, top_k=5):now = datetime.now(timezone.utc)if keyword:candidates = keyword_search(items, keyword) # filter by keyword firstif not candidates: # no hit → local match, not the full setlow = keyword.lower()candidates = [i for i in items if low in i["content"].lower()]else:candidates = list(items) # use the full ranking only without a keywordreturn sorted(candidates, key=lambda i: score(i, now), reverse=True)[:top_k]
For Chinese text, a rough estimate is 1 token per 1–2 characters; apply an equivalent estimate for your own language. Entries that exceed the budget keep only the title and a summary, and the full text is read only when needed.
5.7 Rendering and Writing: Metadata Headers That Can Be Parsed Again
export writes front matter (including id/kind/scope/hash/origin/export_path) so that what lands on disk can be parsed and verified again:
def render_entry(item: dict) -> str:header = ["---",f'id: {item["id"]}',f'kind: {item["kind"]}',f'scope: {item["scope"]}',f'hash: {item["hash"]}',f'origin_tool: {item["origin"]["tool"]}',f'origin_path: {item["origin"]["path"]}',f'export_path: {item["origin"].get("export_path", "")}',"---",]return "\n".join(header) + "\n" + item["content"] + "\n"def export(items, target_root: Path, name_map: dict):written = []for item in items:relative = name_map.get(item["kind"])if not relative:continue # unsupported type on the target; skip and record in the reportpath = target_root / relative.format(id=item["id"].replace(":", "_"))path.parent.mkdir(parents=True, exist_ok=True)if path.exists(): # never overwrite an existing filepath = path.with_suffix(path.suffix + ".new")item["origin"]["export_path"] = str(path) # generated before writing, for retrieve to usepath.write_text(render_entry(item), encoding="utf-8")written.append(path)return written
Two principles: never overwrite existing files (surface same-name entries for a merge decision), and preserve origin and time metadata so the entries remain recognizable in a later migration.
6. Verifying the Migration: Proving It Actually Completed
One key criterion changes here: do not require equal entry counts before and after the migration (deduplication reduces entries by design). Instead, compare across three stages and record the reason for each change.
- Static consistency check. Count entries at three stages — before deduplication, after deduplication, and after export — and compare hashes entry by entry. Entries whose count dropped must be documented in the report as either “merged” or “dropped” with a reason; entries whose hash changed are flagged for manual review. Also scan the target directory to confirm no leftover secrets and no absolute paths.
- Render and load check. Confirm the target tool loads the files without parse errors. Ask at the start of a session “which project constraints are currently in effect?” and compare item by item against the pre-migration state.
- Behavioral consistency check. Prepare a set of regression tasks — modify a function and see whether it follows the migrated code style, add an endpoint and see whether it follows the interface convention, ask about a historical decision that only exists in long-term memory and see whether the answer is accurate. Run the set once before and once after the migration and compare.
Risk control uses a dual-track approach: keep the source tool available read-only for one to two weeks while the new tool serves as the primary one; only after all regression checks pass, clean up redundant entries and keep one complete snapshot.
7. Long-Term Governance: Turning Context into Versionable Assets
- Put it under version control. Store instructions, rules, command templates, and redacted memory in a dedicated repository, review every change, and make rollback trivial.
- Wire it into continuous integration. Have the pipeline validate metadata header completeness, tag validity, and the absence of secrets or absolute paths, and run the deduplication script periodically to produce duplicate and conflict reports.
- Archive and rehearse regularly. Archive snapshots to object storage with versioning enabled; rehearse a full migration in a clean environment once a quarter, record elapsed time and failure points, and keep refining the mapping table and the scripts.
- Make it observable. Log entry counts, failed entries, and validation results to a logging service, set alerts on the failure rate, and stop problems from surfacing only at the moment of use.
8. Closing
Account status, access policies, and tool versions are external variables that a developer cannot control unilaterally. The engineering experience captured in instructions, rules, and long-term memory, however, is a genuine asset that you can own and should own. Turning it from “files bound to one tool’s configuration directory” into “context assets with a unified structure, versions, validation, and instant recoverability” does not depend on one manual copy — it depends on a repeatable engineering pipeline: a unified intermediate representation handles decoupling, deduplication and conflict handling protect quality, path rewriting and capability mapping provide portability, layered validation ensures trust, and versioning plus regular drills keep everything usable in the long run.
To restate: this method is only for migrating a developer’s own configuration, so that work products stay portable and recoverable — not for circumventing platform risk controls or violating terms of service. With that in place, the next time you hit an account limit or switch tools, the task is to run one migration command rather than start over.
