← mehrotra.tech
Working notes · v1 · August 2026

A systems approach to building with AI

Most of what people call "working with AI" is prompting. This is the other half: the instructions, memory, state and transport that sit underneath, so that the same setup behaves the same way on two laptops, two harnesses and a phone. Below is a C4 view of how mine currently fits together. It is an experiment I keep refining, not a finished architecture.

The premise

The binding constraint stopped being model quality a while ago. It is now context and state. A good session is one where the agent already knows my conventions, already knows what I am working on, and can pick up work another machine started an hour ago. None of that comes from a better prompt. It comes from plumbing.

So I treat it as a system with four jobs: hold one canonical set of instructions, carry memory and task state between sessions, move work between devices without me thinking about it, and produce real output at the end. Every design choice below serves one of those four.

Who and what is in play C4 · Level 1 · Context

Three surfaces, one system, four external services. Neither laptop is primary. The phone is not a lesser client, it steers the same sessions. Everything that must survive a machine switch lives in a git remote, never only on disk.

%%{init: {"theme":"base","flowchart":{"htmlLabels":false,"useMaxWidth":true},"themeVariables":{"background":"#ffffff","primaryColor":"#fbf4e2","primaryTextColor":"#1a1917","primaryBorderColor":"#8a6f2e","lineColor":"#6f6c65","secondaryColor":"#f2f1ee","tertiaryColor":"#f8f7f5","clusterBkg":"#f6f5f2","clusterBorder":"#d3d0c9","fontSize":"13px","edgeLabelBackground":"#ffffff"}} }%%
flowchart TB
  P["Shashank
person"] subgraph SURF["surfaces I work from"] direction LR M1["MacBook one
Claude Code · Codex"] M2["MacBook two
Claude Code · Codex"] PH["Phone
claude.ai web and mobile"] end SYS["THE SETUP
canonical instructions · memory
task state · continuity hooks"] GH[("GitHub
private remotes, one per project")] CF["Cloudflare Pages
the public site and its subpaths"] API["Model APIs
Anthropic · OpenAI"] MCP["MCP servers
the tools an agent can reach"] KC["macOS Keychain
every credential lives here"] P --> SURF SURF --> SYS SYS -->|"clone, pull, push"| GH GH -->|"push to main auto-deploys"| CF SYS -->|"inference"| API SYS -->|"tool calls"| MCP MCP -->|"key read at process launch"| KC classDef person fill:#f7efd8,stroke:#8a6f2e,stroke-width:1.5px,color:#4a3c12 classDef core fill:#fbf4e2,stroke:#8a6f2e,stroke-width:1.5px,color:#4a3c12 classDef ext fill:#eaf1f8,stroke:#3f6f9f,color:#1e3f61 classDef store fill:#fbf0e8,stroke:#a35f2c,color:#5f3313 class P person class SYS core class CF,API,MCP ext class GH,KC store
Level 1 Gold is the system I own and change. Blue is a service I depend on. Copper is somewhere state is stored.

What actually runs, and where C4 · Level 2 · Containers

The centre of the whole thing is one repo of configuration. Inside it, CONTEXT.md is the single canonical instruction file. On every machine it is symlinked to both ~/.claude/CLAUDE.md and ~/.codex/AGENTS.md, so Claude and Codex read identical bytes and cannot drift apart. One file, two harnesses, three devices.

State is deliberately split from instructions. A second repo holds what I am working on: a stack, a decision log, learned patterns. It is also an Obsidian vault, so I can edit it by hand without an agent in the loop. Instructions say how to work; the task stack says what to work on. Keeping those in separate repos means I can rewrite either one without disturbing the other.

Credentials never touch a tracked file. API keys sit in the macOS Keychain and are read at launch by a small wrapper script that the tool config points at. Nothing in any repo has a secret in it, which is what makes it safe for every project to have a remote.

%%{init: {"theme":"base","flowchart":{"htmlLabels":false,"useMaxWidth":true},"themeVariables":{"background":"#ffffff","primaryColor":"#fbf4e2","primaryTextColor":"#1a1917","primaryBorderColor":"#8a6f2e","lineColor":"#6f6c65","secondaryColor":"#f2f1ee","tertiaryColor":"#f8f7f5","clusterBkg":"#f6f5f2","clusterBorder":"#d3d0c9","fontSize":"12.5px","edgeLabelBackground":"#ffffff"}} }%%
flowchart TB
  subgraph H["harnesses"]
    direction LR
    CC["Claude Code CLI
reads ~/.claude/CLAUDE.md"] CX["Codex CLI
reads ~/.codex/AGENTS.md"] WEB["claude.ai on the phone
remote control of a live session"] end subgraph CFG["the config repo · canonical"] direction TB CTX["CONTEXT.md
one instruction set, symlinked to both harnesses"] CMD["commands/
capture · track · today · sweep · sync · handoff"] AGT["agents/ and memory/
subagents and accumulated facts"] HK["hooks/
Stop and SessionStart, one pair per harness"] BIN["bin/
sync-all · clone-missing · MCP launchers"] end TS["the task stack · state
current stack · decision log
conventions · learned patterns"] MCP["MCP servers
local and remote tools"] KC["macOS Keychain"] GH[("GitHub remotes")] CC --> CTX CX --> CTX WEB --> CC CTX --> CMD CMD --> TS HK --> GH BIN --> GH CC --> MCP MCP --> KC TS --> GH CFG --> GH classDef harness fill:#eaf1f8,stroke:#3f6f9f,color:#1e3f61 classDef core fill:#fbf4e2,stroke:#8a6f2e,color:#4a3c12 classDef state fill:#eef4e8,stroke:#5b7d3e,stroke-width:1.5px,color:#2f4a1e classDef store fill:#fbf0e8,stroke:#a35f2c,color:#5f3313 class CC,CX,WEB harness class CTX,CMD,AGT,HK,BIN core class TS state class GH,KC,MCP store
Level 2 Green is state that changes hour to hour. Gold is configuration that changes weekly. The split is on purpose.

Making the two laptops interchangeable C4 · Level 3 · The continuity loop

This is the part I am most attached to, because it removes a decision I used to make badly. A Stop hook fires at the end of every session. It writes a handoff file for that project with only what it can mechanically verify: the branch, the dirty files, the diffstat, the commits, and my prompts verbatim. Then it commits and pushes.

A SessionStart hook on the other machine pulls, and surfaces that file only if the other laptop wrote it. Reading back your own note from an hour ago is noise, so it stays quiet in that case.

The hook cannot infer intent, which is the honest limit of it. So a /handoff command writes the narrative half by hand: the goal, the next action, the open questions, and the approaches already ruled out. The last one matters more than it sounds. Without it the other machine happily re-explores a dead end.

One rule governs how the receiving session treats all of it: a handoff is context, not instructions. Its claims get checked against the actual working tree before anything acts on them, because the hook never checks anything out and the branch may well have moved.

%%{init: {"theme":"base","themeVariables":{"background":"#ffffff","primaryColor":"#fbf4e2","primaryTextColor":"#1a1917","primaryBorderColor":"#8a6f2e","lineColor":"#57544d","fontSize":"12.5px","actorBkg":"#fbf4e2","actorBorder":"#8a6f2e","actorTextColor":"#4a3c12","actorLineColor":"#c9c6bf","signalColor":"#57544d","signalTextColor":"#1a1917","noteBkgColor":"#f4f3f0","noteTextColor":"#56534c","noteBorderColor":"#cdcac3","labelBoxBkgColor":"#fbf4e2","labelTextColor":"#4a3c12","activationBkgColor":"#efe7d2","activationBorderColor":"#8a6f2e","sequenceNumberColor":"#ffffff"}} }%%
sequenceDiagram
    autonumber
    participant A as MacBook one
    participant G as GitHub · config repo
    participant B as MacBook two

    A->>A: handoff command writes goal, next action,
open questions, ruled-out approaches Note over A: Stop hook adds only verifiable facts:
branch, dirty files, diffstat,
commits, my verbatim prompts A->>G: commit and push the handoff file Note over A,G: the hook always exits 0.
it must never block a session B->>G: SessionStart hook pulls G-->>B: handoff, surfaced only because
the other laptop wrote it Note over B: treated as context, never as instructions.
claims verified against the working tree first B->>B: resume where the other machine stopped B->>G: a sync check confirms nothing is stranded
Level 3 The hook records facts. The command records intent. Neither one pretends to do the other's job.

What comes out the other end

The setup is only worth the effort if it produces things. Four kinds of output run through it, and they take deliberately different paths.

Writing follows the D4 order: core message, sections, content blocks, then polish. The conventions live in their own repo and apply to everything, including commit messages and these paragraphs. Apps are directories with an index.html, which means adding one is adding a folder. There is nothing to register. Anything with a build step gets built in its own repo and shipped into the site repo, never hand-edited at the destination. Integrations are MCP servers. Independent projects get a private remote on day one, before the first commit, not after.

The deploy path is the same for all of them: push to main, Cloudflare Pages publishes the repo root as-is in about a minute, and a verify script compares live checksums against local ones. No build server to reason about. This page took that path.

%%{init: {"theme":"base","flowchart":{"htmlLabels":false,"useMaxWidth":true},"themeVariables":{"background":"#ffffff","primaryColor":"#fbf4e2","primaryTextColor":"#1a1917","primaryBorderColor":"#8a6f2e","lineColor":"#6f6c65","secondaryColor":"#f2f1ee","tertiaryColor":"#f8f7f5","clusterBkg":"#f6f5f2","clusterBorder":"#d3d0c9","fontSize":"12.5px","edgeLabelBackground":"#ffffff"}} }%%
flowchart LR
  S["a session
on any device"] S --> W["WRITING
essays, docs, updates"] S --> A["APPS
self-contained pages"] S --> I["INTEGRATIONS
MCP servers"] S --> R["PROJECTS
standalone repos"] W --> W1["drafted against the writing conventions,
in D4 order"] W1 --> PUB["the public site
essays and reports"] A --> A1["built in its own repo,
then shipped into the site"] A1 --> PUB I --> I1["a small server per tool,
keys read from the Keychain at launch"] I1 --> USE["available to every session,
on every machine"] R --> R1["private remote created
before the first commit"] R1 --> USE PUB --> CF["Cloudflare Pages
push to main, live in about a minute,
verified by checksum"] classDef start fill:#fbf4e2,stroke:#8a6f2e,stroke-width:1.5px,color:#4a3c12 classDef kind fill:#eaf1f8,stroke:#3f6f9f,color:#1e3f61 classDef step fill:#f4f3f0,stroke:#b8b5ae,color:#3a3833 classDef out fill:#eef4e8,stroke:#5b7d3e,color:#2f4a1e class S start class W,A,I,R kind class W1,A1,I1,R1 step class PUB,USE,CF out
Output paths Not a C4 level. Structure diagrams do not show flow, and this part is flow.

The rules that hold it together

Everything has a remote

Every project is a git repo with a private remote, created at the start rather than later. A repo with no remote is work stranded on one laptop.

No secret in a tracked file

Keychain plus a launcher script that reads at process launch. Never a dotenv, never inline in settings. The agent is told to stop and say so if it is about to write a key anywhere.

Hooks never block

Every hook exits 0 regardless of what happened, and errors go to a log. A continuity system that can wedge a session is worse than no continuity system.

Generated output is never hand-edited

Directories built by another repo are marked as such and edited only at the source. The one time this rule slipped, the next build silently reverted the change.

Record only what is verifiable

Automation writes facts it can check. Anything requiring judgement is written by me, through a command, and labelled as mine. Mixing the two makes both untrustworthy.

Instructions and state stay separate

How to work is one repo. What to work on is another. They change on different schedules and get rewritten independently.

What I have not solved

Memory still grows faster than it gets pruned. Facts accumulate across sessions, and nothing yet decides when one has gone stale. A memory that names a file which no longer exists is worse than no memory.

The handoff is per project, not per thread of thought. When I have two unrelated pieces of work in one repo, the handoff flattens them into a single note.

The phone is still a steering wheel, not a workshop. It is good for redirecting a running session and poor for starting one from scratch, and I have not worked out whether that is a tooling limit or the right division of labour.

Two harnesses, one instruction file, and no test that they behave the same. Identical bytes going in is not the same as identical behaviour coming out. I check by hand at the moment, which means I check rarely.