Local-First Second Brain: Synced Across Devices, Off the Cloud

Built a local-first sync system across two machines so my second brain and its AI assistant are always current and reachable from my phone, with no cloud host in the loop.

GitBashlaunchdTailscaleClaude Code
automationdeveloper-experienceproductivitygreenfield
Lab Build·2026-08-08

The Problem

I keep a personal knowledge base as a local Obsidian vault, and I wanted to reach it — read, edit, and work on it with an AI coding assistant — from my phone, without putting the content on a third-party cloud service. The vault lived in a macOS directory that’s protected by the OS’s file-access controls, which silently blocked the SSH daemon from reaching it, so remote access wasn’t even possible yet. And the deeper requirement wasn’t just “view my notes on my phone” — I specifically needed an interactive AI assistant session against the live vault, not just file read/edit, which ruled out simpler options before I’d even started.

The Solution

I built a local-first sync system between two Macs — a primary laptop and a secondary machine that stays powered on — connected over a private network mesh via SSH, with no cloud intermediary. Both machines run a lightweight sync loop on a timer that commits, pulls, and pushes vault changes automatically, so either machine is always caught up within a few minutes of the other. For phone access, I connect into the always-on machine’s persistent AI assistant session remotely — since the vault there is kept current by the sync loop, the phone session is always working against fresh content. I first tried a simpler cloud file-sync approach and reverted it the same day once I confirmed it solved the wrong problem: it synced files, but couldn’t give me the interactive assistant.

Outcome & Impact

Two machines now stay within a five-minute window of each other with zero manual copying, verified against a 10-point QA checklist after the underlying directory move. The system has run with zero data corruption despite two machines independently committing to the same repository, because the sync loop is designed to abort and log rather than force through a conflict. Mobile access to a fully interactive AI assistant session — working against always-current notes — is now live, solving the actual requirement after a same-day detour into a file-only sync tool proved insufficient.

My Role & Contributions

Aspect Detail
Role Sole designer and implementer
Team size 1
Timeline ~3 days, spanning directory migration, sync design, and mobile access setup
Scope Cross-machine sync architecture, conflict handling, remote-access path, security tradeoff decisions
Key decisions Moving the vault out of a protected directory rather than broadening file-access permissions for the SSH daemon; rejecting a session-hook-triggered sync in favor of a machine-agnostic periodic loop; rejecting a real-time file-sync daemon as unneeded complexity; choosing remote-session access over deeper mobile-native sync

Technical Overview

   Primary laptop (~vault)                  Secondary Mac, always-on (~vault)
   ┌─────────────────────┐                  ┌──────────────────────┐
   │  Obsidian edits     │                  │  AI assistant        │
   │  git repo           │                  │  session (via phone) │
   └──────────┬──────────┘                  └──────────┬───────────┘
              │  timer: add/commit/pull/push           │  timer: add/commit/pull/push
              ▼                                        ▼
        ┌──────────────────────────────────────────────────────┐
        │   direct machine-to-machine git remote, over SSH     │
        │   (private network mesh, no cloud/GitHub remote)     │
        └──────────────────────────────────────────────────────┘
              ▲                                        ▲
              └───────────────── phone ────────────────┘
                    (remote-control into the always-on Mac's
                     persistent assistant session)

Each machine runs its own timer-driven loop, independently: commit local changes, pull the other machine’s changes with a rebase, then push. Neither machine depends on the other’s push to update its working tree — each is self-consistent after its own tick, which sidesteps the need for any central server. A one-time git configuration change on both machines allows a push to land on a currently-checked-out branch without requiring a bare repository. There is deliberately no cloud-hosted remote: both machines reach each other directly over a private network mesh via SSH, so vault content never leaves hardware I control. Phone access works by remoting into the always-on machine’s own interactive assistant session rather than by syncing anything to the phone itself — the sync loop’s only job is keeping that machine’s copy current.

Challenges & Key Decisions

Redefining the actual problem mid-project

My first attempt at mobile access used a cloud file-sync service layered under Obsidian’s mobile app. It worked for reading and lightly editing notes, and I reverted it the same day — it solved file access, not the actual need, which was an interactive AI assistant working against the vault. Recognizing that a working solution was solving the wrong problem, rather than continuing to polish it, was the more valuable call than the eventual sync loop itself.

A protected directory silently blocking remote access

The vault’s original location sat inside a macOS directory that requires explicit, broad file-access permission before any background process — including the SSH daemon — can read it. Granting that permission to unblock one SSH key felt disproportionate to the actual need. I moved the vault to an unprotected location instead, which unblocked SSH cleanly without expanding what any background process could reach.

Choosing the sync mechanism deliberately, not by default

I evaluated three options: triggering sync from the assistant’s own session boundaries, a real-time peer-to-peer file-sync daemon, and a simple periodic git loop. Session-boundary triggers were rejected because most vault edits happen directly in Obsidian, not through assistant sessions — that approach would have silently missed the primary edit path. A real-time sync daemon was appealing but added a second always-running service for a benefit — sub-minute propagation — that doesn’t matter when only one machine is ever actively used at a time. The periodic git loop won because it’s mechanism-agnostic to edit source and self-healing: each machine reconciles on its own schedule regardless of what changed or where.

Guaranteeing that two independently-writing machines never corrupt history

Because both machines write to the same repository on their own schedules, a naive sync loop could silently force through a conflicting rebase and leave the tree in a broken state. The loop instead aborts and logs on any rebase failure rather than resolving it automatically, accepting a small amount of operational visibility work in exchange for a hard guarantee that a conflict never corrupts either machine’s copy.

Lessons Learned

  • Solving the stated problem isn’t the same as solving the real one. The file-sync detour was a legitimate solution to a slightly wrong problem statement; catching that early, by testing against the actual use case instead of the assumed one, saved a much larger wasted effort.
  • Match the mechanism to the actual usage pattern, not the most capable option. A real-time sync daemon was the “better” tool in the abstract, but a periodic loop was the right one given non-concurrent, single-user access — capability that goes unused is just added surface area.
  • Broadening a permission to work around a blocker is rarely the smallest fix. Moving the protected data was less invasive than expanding what a background service could access, even though the permission change would have been a single command.