Skip to the content.

nsync

Claude plugin that treats your Notion workspace like a git repository: pull, edit locally, diff, commit.

The problem

If you work on Notion docs alongside Claude, you’ve felt this:

nsync makes the seam between Claude and Notion behave like git. Pull your tree once, edit locally with the tools you already use, see a real diff before changes go live, and let content hashes catch surprises before they ship.

What you get

Mental model

Like git, but the remote is a Notion page tree.

my-docs/                       ← sync root (your local working dir)
├── .nsync/                    ← like .git/; local state, do not edit
│   ├── config.json            ← parent page identity
│   ├── manifest.json          ← per-page UUID + hashes
│   ├── ignore                 ← which .md files to skip
│   └── snapshots/<id>.md      ← last-synced baselines (drive safe updates)
├── welcome.md                 ← mirror of a Notion page
└── engineering/
    ├── index.md               ← body of "Engineering" page itself
    ├── standards.md
    └── onboarding.md

local_hash and remote_hash in the manifest are your indices. The snapshots are the staging baseline used to compute commits that touch only the markdown, never rich blocks.

Install

Prerequisite: Python 3

nsync shells out to a small bundled helper (scripts/nsync.py) for the deterministic work (content hashing, markdown normalization, diffing), so the model never does it by hand (slow, and unreliable for hashing). You need Python 3 on your PATH (python3 --version should print 3.x). It’s standard-library only: no pip install, no virtualenv. macOS and most Linux distros already ship it.

Prerequisite: connect Notion

The plugin uses Anthropic’s built-in Notion connector. Connect it once before installing:

You only do this once per workspace. No environment variables, no integration tokens, no per-page sharing; the OAuth connector handles all of that.

Claude Code

claude plugin marketplace add stanleynguyen/nsync-skill
claude plugin install nsync@nsync

Restart your session so the slash commands register. Verify with claude plugin list; nsync@nsync should appear as enabled.

Claude Cowork

  1. Open the Plugins panel in your Cowork workspace.
  2. Add a marketplace and paste stanleynguyen/nsync-skill (or the full URL https://github.com/stanleynguyen/nsync-skill).
  3. Once the marketplace appears, install the nsync plugin from it.
  4. Reload the workspace so the slash commands surface in /help.

Local development

If you’ve cloned the repo locally, point the marketplace at the checked-out path:

claude plugin marketplace add /path/to/nsync-skill
claude plugin install nsync@nsync

Your first sync

Five-minute walkthrough. Pick a parent page in Notion you’re willing to experiment on (or create a throwaway one) and copy its URL.

mkdir my-docs && cd my-docs
/nsync:init https://www.notion.so/workspace/My-Docs-fb1d8c3a5e214f708a239c4b6d8e1f02

You’ll see the sub-tree mirrored into local .md files, plus a .nsync/ state directory. Pages with sub-pages become folders containing index.md (the parent’s own body) alongside the children.

Open one of the files in your editor of choice and make a small change.

/nsync:status

Shows Modified (local): welcome.md. Same vocabulary as git status.

/nsync:diff

This is the dry-run. Rich blocks (callouts, embeds, images) show up as [rich block: <type>] (not synced) placeholders so the markdown delta reads cleanly. If Claude wrote something you don’t want, this is where you catch it.

/nsync:commit

Push lands as a snippet-level update: only the markdown lines that changed get touched, every rich block stays exactly where the Notion author put it. Reload the page in Notion to confirm.

From here, just edit and commit. Open a Claude session anywhere in my-docs/ and it has the entire tree available, no link pasting required.

Commands

Command What it does
/nsync:init [url] Initialize CWD as a sync root mirroring the given Notion page. Prompts for the URL if omitted.
/nsync:status Show modified / added / deleted / remote-newer files. Read-only.
/nsync:diff [path...] Unified diff between local files and Notion. Pass any number of file or folder paths to scope (e.g. /nsync:diff engineering/ welcome.md). With no args, diffs every non-clean page. Read-only.
/nsync:pull Pull remote changes. Auto-merges clean-side updates; prompts per conflict.
/nsync:commit [--force <path>...] Push local changes to Notion. Refuses if remote has unpulled changes (override with --force).

Depending on your Claude Code build the slash form may appear as /nsync:init or /init (with a (plugin:nsync) label). Check /help after install.

Workflow patterns

Iterating on a doc with Claude. Open a session anywhere in your sync root. Ask Claude to refine engineering/onboarding.md. Run /nsync:diff to inspect what Claude changed; /nsync:commit when you’re happy. The doc never leaves a reviewable state.

Multi-doc refactor. Renaming a concept across 20 pages? Use sed -i or have Claude do the substitution, then /nsync:diff engineering/ to scope the dry-run to one subtree before pushing. Bulk edits Notion’s UI can’t do cleanly.

PR-reviewable doc changes. Commit .nsync/manifest.json and your local tree to git. The diff that lands in your pull request is human-readable markdown, not Notion’s opaque block IDs; reviewers see exactly what changed, branch-based experimentation works, and rollback is a git revert away.

Create a sub-page and link it in place. A new sub-page normally lands as a Notion child block at the page foot. To place the link mid-document instead, create the .md and drop a placeholder line where you want it, the managed child-link form minus the page id: [Roadmap](./roadmap.md) <!-- nsync:child -->. On /nsync:commit, nsync creates the page and backfills the real id in place: no duplicate, no broken link. The same placeholder also positions a link to an already-existing child. Caveat: this sets the local position only; Notion still renders the child block at the page foot (in-body reordering on the Notion side is a future /nsync:mv).

How it works

If you check .nsync/ into git, ignore .nsync/snapshots/ (large, regeneratable), .nsync/conflicts/ (transient), and .nsync/tmp/ (scratch space cleaned up after each run). Keep .nsync/config.json, .nsync/manifest.json, .nsync/ignore checked in for team visibility.

Default ignore patterns

/nsync:init writes a small .nsync/ignore with these patterns (because non-.md files are already out of scope, this list filters specific markdown files only):

README.md
CHANGELOG.md
LICENSE.md
CONTRIBUTING.md

Edit .nsync/ignore to add your own. Syntax is gitignore-compatible.

Limitations

Contributing

Open repo at https://github.com/stanleynguyen/nsync-skill. Issues and PRs welcome.

Report bugs or request features

File an issue at https://github.com/stanleynguyen/nsync-skill/issues. Minimal reproduction beats prose; the throwaway-page recipe in the end-to-end checklist below works well as a starting template.

Submit a PR

Open at https://github.com/stanleynguyen/nsync-skill/pulls. One concern per PR, please. For non-trivial design changes, open an issue first.

End-to-end checklist

Run these against a throwaway Notion parent page before requesting review.

Create a Notion parent page “nsync-test” with three sub-pages:

Make sure the Notion connector is connected (see Install → Prerequisite above), then run through the checklist:

  1. mkdir /tmp/nsync-test && cd /tmp/nsync-test
  2. /nsync:init <url>: expect .nsync/, welcome.md, engineering/index.md, engineering/standards.md, engineering/onboarding.md, notes.md.
  3. /nsync:status → “Working tree clean.”
  4. Edit welcome.md, add a paragraph. /nsync:diff shows the addition. /nsync:commit pushes it; reload Notion and confirm.
  5. In Notion, edit “Standards”. Wait a few seconds. /nsync:statusRemote-newer. /nsync:pull → auto-merge.
  6. Edit engineering/onboarding.md locally AND in Notion differently. /nsync:pull → conflict prompt. Pick [E]dit, resolve in the scratch buffer, then /nsync:commit.
  7. Rename welcome.mdintro.md. /nsync:status → rename prompt. Confirm. /nsync:commit.
  8. In Notion, add a new sub-page “Roadmap”. /nsync:pullroadmap.md appears locally.
  9. Edit notes.md’s markdown portion locally. /nsync:diff shows only that hunk plus [rich block: callout] (not synced). /nsync:commit → push; reload Notion and confirm the callout is still present.
  10. Delete notes.md. /nsync:commit → orphan / manual-trash / restore prompt. Pick orphan.

If all 10 steps behave as expected, your change is ready for review.