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 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.
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
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
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
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
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.
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.
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.
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.
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.
How to work is one repo. What to work on is another. They change on different schedules and get rewritten independently.
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.