Sync a memory from a Git repo¶
A Hadron memory can pull its node graph from a GitHub repository. You author nodes as Markdown files with YAML frontmatter, push to GitHub, and sync — every change is version-controlled and the repo becomes the source of truth for the memory's content.
This guide covers the repo layout Hadron expects, GitHub authentication, the sync triggers and what they do, conflict behavior, and how to convert an existing memory to Git-backed.
When this is the right shape¶
Reach for Git-sync when:
- The memory's content is authored in long form — domain reference, knowledge-base articles, decision records, agent prompts.
- You want review on memory changes before they land — pull requests are the natural review surface.
- The memory's content benefits from history — who changed what, when, why.
It's the wrong shape when:
- The memory is mostly machine-written by agents at chat-time (per-user personal memories, chat history, extracted facts) — those should stay database-resident.
- The content needs to be encrypted at rest — a plaintext GitHub repo undoes the encryption, even if the platform allows the configuration.
- You need cross-memory edges that a flat repo can't express — Hadron tracks unresolved cross-repo references in a pending-edge table, but heavy reliance on them suggests the memory boundary is wrong.
Prerequisites¶
- A GitHub repository you control (private is fine).
- A Hadron organization with the GitHub App installed on the repo. Without an installation, the platform has no token to clone with. Public repos are not a special case — they still require the App installation.
- Org admin role on the Hadron org.
Step 1: Lay out the repo¶
Hadron treats the repository as an Obsidian-style vault of nodes — one self-contained Markdown file per node, with YAML frontmatter for metadata and edges. (You can browse the result in Obsidian — see Use Obsidian to view/edit memory.)
A minimum repo:
.
├── README.md ← repo root: the memory manifest
├── architecture.md
├── decisions.md ← parent node "decisions"
├── decisions/
│ ├── pagination-buffer-size.md
│ └── retention-policy.md
├── glossary.md ← parent node "glossary"
└── glossary/
├── stage.md
└── topic.md
Each .md file becomes a node. Frontmatter carries metadata, and
edges to other nodes live inline under nodes::
---
id: 019d-7e7c-… # UUIDv7, optional — generated on first sync if absent
name: Pagination buffer size
description: Why /api/users caps page size at 500.
tags: [pagination, api]
seq: 10
nodes:
- id: 019d-9f2a-…
loc: decisions:retention-policy
rel: relates-to
---
# Pagination buffer size
The buffer was set when /api/users was an in-memory list. As of
2026-02 the endpoint is Postgres-backed and the cap is misleading…
Set type: when the node isn't the default info (which is omitted);
see Node types for the full set.
A node with children is written as <name>.md beside its <name>/
folder — decisions.md sits next to decisions/. The file is the
node; the folder holds its children. Because a node's path doesn't
change when it gains or loses children, renames don't churn the graph.
The only README.md is at the repo root, where it carries the memory's
manifest (URN, name, description) rather than a node.
Edges¶
Edges live inline in each node's frontmatter, under a nodes:
array — so a node is one self-contained file:
nodes:
- id: 019d-9f2a-… # target node's id
loc: decisions:retention-policy # target's loc, for readability
rel: relates-to # the relationship label
- id: 019d-3c44-…
loc: glossary:stage
rel: defines
The importer resolves each edge by the target's id; the loc rides
along for human readability. Add condition: (a JSONLogic expression)
to gate an edge or priority: (lower fires first) to order resolution
— both round-trip through the repo. Cross-repo references — pointing at
a node in a different memory — are valid; they sit in the platform's
pending-edge table and resolve once the target is synced into its
memory.
Back-compat for hand-authors. Hadron still imports two older
layouts even though it no longer writes them: parent nodes as
<folder>/README.md, and edges in a .<file-stem>/edges.yaml sidecar
beside the node. If both a sidecar and an inline nodes: array exist
for a node, the sidecar wins — so when you migrate a node to the
inline format, delete its old sidecar. The oldest format
(directoryName/.node/node.yaml) also still loads. New repos should
use the self-contained frontmatter shape above; the older formats get
no new features.
Step 2: Authenticate¶
Hadron's GitHub integration uses a GitHub App model:
- Install the Hadron GitHub App on the repo (or on the org that owns it). The App is the same one your Hadron org's admin uses for any other Git-sync; you don't install one per memory.
- The Hadron org records the installation ID on its
Organization.githubInstallationIdfield at install time. From then on, every sync mints a short-lived access token (RS256-signed JWT → 1-hour token) and clones with it. - Self-hosted Hadron: an org can override the App by setting
githubAppIdand an encryptedgithubAppPrivateKeyon its Organization row, or by settingGITHUB_APP_IDandGITHUB_APP_PRIVATE_KEYenv vars on the server. The lookup order is org-specific → env-var fallback.
If a sync returns a clone error, the App probably isn't installed on the target repo or the installation has been revoked. Re-install via the GitHub UI.
Step 3: Connect the memory¶
In the portal:
- Open the memory's detail page → Settings.
- Click Edit.
- Expand the GitHub integration section.
- Fill in:
- Repository URL —
https://github.com/<owner>/<repo>or the SSH form. HTTPS is preferred. - Read branch — the branch to clone. Default
main. - Write branch — only set this if you'll push back from Hadron (see Pushing back). For pure read-only sync, leave it blank.
- Save. The memory's
source,readBranch, andwriteBranchfields are now set; nothing has been cloned yet.
If you don't see the GitHub integration section, your role on the org is below admin. The memory edit form gates it on org admin.
Step 4: First sync¶
- On the same memory page, click Sync from Git. The same
action is available via the
h-sync-graphMCP tool if you'd rather drive it from a coding agent. - The server clones the repo to its on-disk cache, walks the tree, and upserts every node and edge it finds.
- The page refreshes; the Sync status row shows
OK, the Last synced timestamp updates, and any pending cross-repo edges show in Pending edges.
If the sync fails:
syncStatus = ERRORandsyncErrorcarries the message. Common causes: repo not found (App not installed), branch not found (typo inreadBranch), invalid frontmatter on a file (missing required fields).- The memory's existing nodes are untouched on failure — sync is all-or-nothing for the upsert phase.
What sync does to existing nodes¶
Hadron treats Git as the source of truth for any memory that has a
source set:
- Nodes in the repo are upserted by
(loc, memoryId). - Nodes that exist in the database but not in the repo are deleted. There is no merge, no diff, no per-file intervention.
This is destructive on purpose — Git-backed memories are meant to
mirror the repo. If you have nodes in the database you want to
keep, add them to the repo before the next sync, or remove
the memory's source to disconnect it.
Soft-deletes are honoured the same way they would be from any other write path: the nodes go to the trash and are recoverable within the configured window.
Step 5: Author and re-sync¶
The normal authoring loop:
- Edit a node's Markdown file in your editor (or in the GitHub web UI).
- Commit and push.
- In the portal, click Sync from Git — or call
h-sync-graphwith the memory's URN. - Confirm the change in the memory's node browser.
There's no webhook, no polling, no auto-sync in v1. Every
sync is explicit. If you want CI-driven sync, wire your repo's
post-merge action to call the syncMemory GraphQL mutation.
Pushing back¶
If you set writeBranch, the memory exposes a Push to Git
button on the same Settings page. The button serialises the
current memory tree back to Markdown + YAML, writes it on the
write branch, and commits with the org's GitHub App identity.
Two important constraints:
- The push mutation (
pushMemoryToGit) is admin-only. - There is no UI for setting up periodic auto-push in v1. Push-to-Git is for occasional flushes (e.g., agent-curated knowledge that has stabilised), not for every node mutation.
Converting an existing memory¶
A memory created without a Git source can be converted later:
- Open the memory → Settings → Edit.
- Fill in the GitHub integration fields.
- Save. (The memory's existing nodes are still untouched at this point.)
- Important: before clicking Sync, export the existing nodes to the repo first. Otherwise the first sync will delete every database-only node (per the rule above).
The simplest export path is to call pushMemoryToGit once
(admin) — it serialises the current memory to the write branch.
Open a PR to merge that into the read branch, then sync. After
that, the repo and the memory are aligned and the normal
edit-and-sync loop applies.
Watching sync state¶
Every memory carries four sync-state fields (visible on the
Settings page or via the Memory GraphQL type):
| Field | What it tells you |
|---|---|
syncStatus |
PENDING, SYNCING, OK, or ERROR. |
syncError |
Free-text error message when status is ERROR. Plain text for now — no structured error code. |
lastSyncedAt |
Timestamp of the last successful sync. |
pendingEdgeCount |
Cross-repo edges that haven't been resolved yet. |
A non-zero pendingEdgeCount is normal during a multi-memory
rollout — the targets simply haven't been synced into their own
memories yet. Re-sync those memories and the count drops.
Limitations¶
- Binary assets are not synced. Assets uploaded via the asset-upload tool live in object storage, not the database; Git-sync only walks Markdown + YAML.
- Encrypted memories can technically have a Git source, but syncing plaintext content into a public or shared repo defeats the encryption. Treat encrypted memories as database-resident.
- Personal memories can technically have a Git source, but the data is per-user — putting it in a shared repo crosses the privacy boundary. Don't do this without an explicit reason.
- No webhook ingestion. Every sync is explicit
(
h-sync-graphor the portal button). Hook your CI up manually if you want automation. - No per-file conflict resolution. Sync is upsert-or-delete
by
(loc, memoryId). Drift handling is out of scope for v1 — if you need it, manage divergence with PR review on the repo side, not in the platform.
Troubleshooting¶
| Symptom | What to check |
|---|---|
Sync fails with repository not found |
The Hadron GitHub App isn't installed on the target repo, or the install was revoked. Re-install. |
| Sync succeeds but nodes don't appear | Check syncStatus = OK and read the node browser. If files in the repo lack required frontmatter (name), they're skipped silently. |
| Pre-existing database nodes vanished | Expected — see What sync does to existing nodes. The memory is now Git-mirrored; bring nodes into the repo before sync. |
| Edges break after a rename | Edges resolve by the target's id, which is stable across renames — keep the id in each nodes: entry and edges survive. A renamed file does get a new loc, so update any citations that point at it. |
pendingEdgeCount keeps climbing |
Cross-repo references whose targets aren't synced yet. Sync the target memories; the resolver passes auto-resolve on next sync. |
| Push-to-Git button missing | Either writeBranch is unset or your role is below admin. |
Related¶
- Use Obsidian to view/edit memory — open the synced or exported repo as an Obsidian vault, and make citation-numbered nodes readable.
- Adding nodes to a memory — direct authoring through the portal UI for memories that aren't Git-backed.
- Memory access — gates that apply to Git-synced memories the same as any other.
- Building an agent — attach a Git-backed memory to an agent so it shows up in the agent's context.