Building hn: A Technical Walkthrough of a Hashnode CLI Sync Engine
How we built a stateful sync engine to reconcile local Markdown with the Hashnode GraphQL API.
I’m a visually impaired software engineer who finds deep joy in exploring ideas and uncovering unexpected connections. I’m drawn to patterns that often go unnoticed. I love finding those veered threads that do not seem related until they suddenly come together. I write to make sense of what I learn and enjoy breaking things down for others. For me, it is about connecting ideas, sharing the process, and letting curiosity lead the way.
Building hn: A Technical Walkthrough of a Hashnode CLI Sync Engine
This post documents the design and implementation of hn, a command-line tool for managing a Hashnode blog using local Markdown files. Rather than focusing on the end result, this walkthrough examines the internal mechanics of the system: how state is tracked, how changes are detected, and how local files are reconciled with a remote GraphQL API.
The project evolved around a single constraint: Hashnode is not Git, but authors expect Git-like guarantees when working locally. The implementation reflects that mismatch.
Treating a Blog as a Stateful System
Hashnode posts live remotely, identified by opaque IDs and mutated through API calls. Local Markdown files, on the other hand, are mutable, movable, and version-controlled by Git. Bridging these two worlds requires explicit state tracking.
hn models this as a sync engine, not a publishing shortcut.
The repository contains three categories of data:
- Author content — Markdown files under version control.
- Local system state — stored under
.hashnode/. - Remote state — represented indirectly through IDs and checksums.
The CLI does not infer relationships implicitly. Every relationship is recorded. This workflow treats content management like a version-controlled state machine. Local edits are captured explicitly through staging, and every change is linked to a snapshot—an immutable record of that exact content. Staging is not caching; it represents intent, while snapshots provide content verifiability and history.
Local State Layout
A typical repository using hn looks like this:
the-reflective-engineer/
├── .hashnode/
│ ├── snapshots/
│ ├── blog.yml
│ └── hashnode.stage
├── hashnode.sum
├── 2025/
│ ├── 07/
│ └── 11/
├── ai/
└── cli/
Each component serves a distinct purpose.
.hashnode/blog.yml
Stores publication-level configuration: Publication ID, URL, and authentication metadata. It is written once during hn init and reused for all subsequent operations.
hashnode.sum
Acts as a ledger, recording the last-known synchronized state between local files and Hashnode posts. It ensures deterministic updates and prevents accidental overwrites. The ledger is append-only in practice, updated only after successful remote operations.
Example (simplified):
articles:
2025/07/example.md:
post_id: 68680d57ccd223957ad25a9d
checksum: 5de2d292...
slug: example-post
.hashnode/snapshots/
Snapshots are content-addressable, immutable copies of Markdown files, named by their SHA-256 checksum. They serve two purposes:
- Detecting real content changes.
- Allowing recovery if a staged change is aborted.
Why Checksums Instead of Timestamps
Git repositories frequently invalidate file modification times (e.g., during git clone, branch checkouts, or rebases). Relying on timestamps would cause false positives. hn instead computes SHA-256 hashes and compares them to the ledger: current_checksum != last_synced_checksum. No heuristics are involved—only content identity matters.
Staging: Capturing Explicit Intent
Staging is the declarative workspace of hn. When you stage a file or directory, the system records your intent to create, update, or delete content. Each staged item references a snapshot, capturing the content at the time of staging.
This separation of intent from execution ensures that each action is explicit, auditable, and reversible. Staged entries answer:
- What is this? (type)
- Which entity? (key)
- What content version? (checksum)
- What action is intended? (operation)
Simplifying the State Machine: Modified vs Delete
A sophisticated design choice in hn is the simplification of operations. In file-based synchronization systems, Create is just a special case of Modify. Explicitly tracking "create" in the staging area is often redundant because the ledger already indicates whether a file exists remotely.
1. The "Existence" Heuristic
hashnode.sum acts as the source of truth:
- If a file is in the ledger: The system knows it exists remotely.
- If a file is NOT in the ledger: The system assumes it is new.
Therefore, the staging area only needs modified and delete.
- Case A: Stage
op: modifiedfornew-post.md. Planner sees it is absent in the ledger → infers CREATE. - Case B: Stage
op: modifiedforold-post.md. Planner sees it exists in the ledger → infers UPDATE.
2. The Logic Table Here is how the planner determines the correct GraphQL mutation:
| Staged Op | Ledger Status | Inferred Action (Plan) | GraphQL Mutation |
modified | Not Found | Create | createPublicationStory |
modified | Found | Update | updateStory |
delete | Found | Delete | removeStory |
delete | Not Found | Error / No-op | (User attempted to delete non-existent post) |
3. Why Delete Must Be Explicit
Absence ≠ Intent. A deleted local file might be accidental. Because deletion is destructive, it requires a specific instruction. Only op: delete signals the verified intention to remove a post.
Key Takeaway:
- Modified signals: "I want this content to exist on the server."
- Delete signals: "I want this content to not exist on the server."
Planning: Read-Only Analysis
The hn plan phase reads the working tree, hashnode.stage, and hashnode.sum. It produces a categorized preview of intended operations without mutating state.
Below is the workflow showing how Staging and the Ledger interact to produce the Plan:
Applying Changes: Deterministic Execution
hn apply executes staged operations in order:
- Acquire a local lock.
- Validate staged checksums against snapshots.
- Execute GraphQL mutations.
- Update
hashnode.sumledger. - Release the lock.
If any step fails, the ledger is unchanged, maintaining restartable operations. Snapshots ensure that content is exactly what was staged, preventing accidental overwrites.
Snapshots and Garbage Collection
Snapshots accumulate naturally. hn gc removes unreferenced snapshots—those not present in the ledger or the staging area. Since snapshots are content-addressable and immutable, cleanup is safe and reversible.
Closing Notes
hn operates as a state machine for content rather than a simple file uploader. Its core principles are:
- Record explicit intent through staging.
- Capture immutable snapshots of content.
- Execute deterministic, auditable plans.
- Maintain ledger consistency between local and remote state.
By separating intent, content, and execution, hn ensures that managing a Hashnode blog locally is safe, predictable, and inspectable.

