Skip to content

Sync a memory from a Git repo

PortalMCPAPIIntermediate~20 min

You can keep a memory's content in a GitHub repository: each node is a Markdown file, changes go through pull requests like any other code, and Hadron pulls them in whenever you push to the repo's default branch. Your history, reviews and blame all live where your team already looks.

Setting it up is done in the portal, once per organization and once per memory. After that, a push is enough, and you or your agent can also trigger a sync on demand.

When this is the right shape

It suits a memory whose content is written by people, in long form — reference material, decision records, agent prompts — and where you want review before a change lands.

It is the wrong shape for:

  • Memories agents write as they work (chat history, extracted facts, working memories). Those belong in the database.
  • Encrypted memories. Sync refuses them: a plaintext repo would undo the encryption.
  • A memory with no organization, such as a free-standing memory you own as a user. Sync needs the memory to belong to an organization, because the GitHub connection is the organization's.

What you need

  • A GitHub repository. Private is fine.
  • To connect your organization to GitHub, and a memory to a repo: admin rights in the organization.
  • To sync: contributor rights in the organization (or, through an agent, write access to the memory).

Step 1: Connect your organization to GitHub

This happens once per organization.

  1. In the portal, click your organization's name under Organization in the sidebar, and open its Integrations tab.
  2. Next to Add:, choose GitHub, then click Install Hadron GitHub App.
  3. On GitHub, choose which repositories the App may read, and install it.

Back in the portal, the tab reports Hadron GitHub App installed and linked to this organization., and the GitHub card reads Connected.

Public repos need the App too

Hadron reads every repo through the App installation, public or not. Without one, a sync does nothing at all — see Troubleshooting.

If you need GitHub Enterprise Server or your own credentials, the same tab has Use your own GitHub App instead, with setup steps.

Step 2: Connect the memory to the repo

  1. Open the memory in the portal and go to its Integrations tab.
  2. In the GitHub card, fill in:
    • Git source URL — https://github.com/<owner>/<repo>.git, or the SSH form git@github.com:<owner>/<repo>.git. Only GitHub is supported.
    • Read branch — the branch Hadron reads, usually main. Set it explicitly. A blank Read branch starts out reading the repo's default branch, but after Hadron first writes an edit back it reads the write branch instead — your pushes to main stop syncing while the status still reads OK (a known bug, hadron-server#1566).
    • Write branch — where Hadron writes changes back. See Sending changes back to GitHub before you leave this blank.
  3. Click Save.

Step 3: Sync

On the same Integrations tab, click Sync from Git in the Sync section. When it finishes you see Memory synced., and the Status line shows the sync status and when it ran.

After the first sync, you rarely need the button:

  • A push to the repo's default branch syncs the memory automatically. If you set a Read branch other than the default branch, pushes do not trigger a sync; sync by hand.
  • Hadron also syncs every Git-backed memory when the server starts.

From an agent. An agent connected to Hadron can run a sync with its hadron_sync_graph tool, given the memory's full URN (hrn:mem:<org>:<memory>) and write access to the memory. An agent cannot connect a memory to a repo or push back — those two are portal-only today.

From CI or a script, call the GraphQL syncMemory(id:) mutation with the memory's id or URN.

What a sync changes

A sync makes the memory match the repo, for the nodes that came from the repo:

  • New and changed files create or update nodes. A node is matched by its id first, so a renamed or moved file updates the same node instead of creating a new one.
  • A file deleted from the repo deletes its node, permanently. It does not go to trash. Restore the file and sync again to bring it back.
  • Nodes created in the portal or by an agent are left alone. A sync only removes nodes it created itself.
  • Some nodes are never removed: a minted spec and a Channel's chat both survive their file disappearing, and the sync reports them.
  • A failing sync changes nothing. If the sync itself fails, the status reads ERROR and nothing is written. If removing a node would break edges from another memory, the whole sync is refused rather than leaving those edges dangling.

A broken file can delete its node

A file Hadron cannot read — no frontmatter, or frontmatter that is not valid YAML — is skipped with a warning, and the sync still succeeds. If that file was synced before, its node is now missing from the repo's view, so the sync deletes it, permanently, while every other file updates and the status reads OK. Check your frontmatter before you push, and restore the file and sync again if a node vanishes.

Sending changes back to GitHub

Hadron writes in two different ways, and they use different default branches:

  • Edits made in Hadron to a synced node are committed automatically to the Write branch — or to hadron-updates if you left it blank. Merge that branch into your read branch, or the next sync will overwrite those edits. Edits are only written back after the memory has synced at least once.
  • Push to Git, on the memory's Integrations tab, writes the whole memory to the repo. It needs admin rights in the organization. It pushes to the Write branch — or to main if you left it blank.

So set a Write branch unless you want Push to Git committing straight to main.

Deleting a node in Hadron does not delete its file. Moving a node does move the file.

Checking a sync

The memory's Integrations tab (Status) and its Info tab (Sync status) show the sync status: PENDING, SYNCING, OK or ERROR. The portal does not show why a sync failed; the reason is on the memory's syncError field, readable through GraphQL along with lastSyncedAt and pendingEdgeCount. Your organization's memories list also has a Last synced column.

Writing files Hadron understands

The repo is an Obsidian-style vault: one Markdown file per node, with YAML frontmatter for its metadata and edges. (You can open it in Obsidian — see Use Obsidian to view/edit memory.)

.
├── README.md                ← the memory's root node
├── entity-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

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, so a node's path does not change when it gains or loses children.

A node file:

---
id: 019d-7e7c-…   # optional; generated on first sync if absent
name: Pagination buffer size
description: Why /api/users caps page size at 500.
tags: [pagination, api]
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…
  • Give every file a name, and keep its frontmatter valid YAML. A file with no name fails the whole sync (ERROR, nothing written). A file that cannot be read at all is skipped with a warning — and if it was synced before, its node is deleted. See What a sync changes.
  • An id you omit is generated in Hadron. It is not written back to your file, so add it yourself if you want renames to be tracked reliably.
  • runnable: and role: set whether the node is a runnable task and its role.
  • The node type is not read from the file, except type: link. Nodes synced from Git get the default type. seq: is not read either.

Edges

Edges live in the node's frontmatter, under nodes::

nodes:
  - id: 019d-9f2a-…                  # the target node's id
    loc: decisions:retention-policy  # the target's loc, for readability
    rel: relates-to                  # the relationship (the edge's name)

Each edge is resolved by the target's id; the loc is there for people. An entry can also carry description:, runnable:, condition: (a JSONLogic expression that gates the edge) and priority: (lower fires first).

An edge may point at a node in another memory. It waits as a pending edge and connects once that memory is synced.

Older layouts still load, though Hadron no longer writes them: parent nodes as <folder>/README.md, edges in a .<file-stem>/edges.yaml sidecar (which wins over inline nodes: — delete it when you migrate), and the oldest <folder>/.node/node.yaml.

Converting an existing memory

You can connect a memory that already has content. The first sync adds the repo's nodes and leaves the existing ones in place — it only ever removes nodes it created itself. To move existing content into the repo, push it first with Push to Git (with a Write branch set), merge that branch, then sync.

Limitations

  • Only Markdown files become nodes; images and other files are ignored.
  • Encrypted memories cannot be synced. The sync refuses with "Git sync cannot write an encrypted memory without its data key."
  • No connect or push from an agent. Both are portal or GraphQL only.

Troubleshooting

Symptom Cause
The portal says Memory synced., but nothing changed and the status stays SYNCING The organization has no GitHub App installation. Do Step 1.
Status ERROR Read the memory's syncError through GraphQL; the portal does not show it. Nothing was changed.
Save fails on the GitHub card Connecting a repo needs admin rights in the organization.
A push to GitHub did not sync The memory's Read branch is not the repo's default branch. Sync by hand.
Pushes stopped syncing, but the status still reads OK The Read branch is blank and Hadron has written an edit back, so it now reads the write branch (#1566). Set the Read branch to main and sync.
Your Hadron edits disappeared after a sync They were on the write branch, and it was not merged into the read branch first.
Push to Git committed to main No Write branch is set.
An edge is missing Its target is in another memory that has not synced yet.