← Posts

It’s time to rearchitect the development environment

Our dev machines are a hot mess. Let’s fix that.

July 5, 2026
It’s time to rearchitect the development environment

Your dev box is an API nobody designed.

(It shows.)

Every compiler, linter, formatter, test runner, bundler, script, editor extension, and coding agent consumes that API. The problem is, it’s not written down. It’s the largely implicit, halfway accidental composition of the filesystem, environment variables, local secrets, package caches, subprocesses, symlinks, ports, daemons, shell startup files, generated artifacts, editor state, version control credentials, and whatever else happens to be reachable from the current process. No one sat down and thought this through as a whole; it just accreted, one thing after another, after another, after another

For decades, a development environment has meant “a machine where the repo works.” At first that machine was a physical computer. Over time, it moved slightly up the stack, becoming a cloud VM, dev container, or hosted agent sandbox. That bought some portability, but it never fixed the underlying mess — and the cloud makes us pay for that mess many times over, because every task might rebuild the environment from scratch: Clone the repo. Install dependencies. Restore caches. Start services. Run the tool. Scrape stdout. Throw most of it away. Repeat, repeat, repeat.

This isn’t how most software works. Modern products don’t onboard a new user by redeploying the whole service topology and leaving dedicated servers running while the user thinks about which UI theme to choose. Instead, they create logical user-specific state inside shared infrastructure, amortizing fixed costs across every user. But the former is effectively how we onboard each new development worker today (whether human or agent): stamp out a workspace, hydrate a private copy of the codebase, start services, run tools, wait, and discard it shortly after. This may seem “normal,” but that’s only because we’ve been doing it for so long. I think it’s neither necessary nor reasonable.

A dev environment has traditionally been one person’s computer, and for a single human that (mostly) worked fine. But now it might be thousands of short-lived agent tasks, each pretending it deserves its own machine. That’s a silly way to spend a data center.

Our modern development environment is very expensive spaghetti. It’s well past time to destructure it: to tease things apart, find the boundaries, and reassemble with the care it deserves. That means giving up the assumption of a shared operating system substrate, then replacing it with intentional contracts. Based on some initial explorations, I suggest the following principles for doing so.

Link to Code becomes dataCodebecomesdata

We pretend code is just files, then make every tool laboriously rebuild the underlying structure.

Deep in their souls, codebases aren’t just text. They’re semantic graphs of symbols, references, modules, and the like. Text files are a useful projection, but we shouldn’t need to string match over the file system to navigate this structure; we should be able to traverse its meaning.

This isn’t a new idea, of course. There’s plenty of prior art: Glean stores queryable facts about source code, Kythe builds cross-language code graphs, CodeQL supports structural pattern matching, and SCIP standardizes code intelligence indexes. The problem with most existing technologies is that they’re optional, read-only indexes that sit adjacent to the source of truth; they help tools understand code, but they’re separate from the code itself. To fully treat code as data, we must use structured querying as the canonical read method — and we must write changes this way too. Unison is probably the closest manifestation of this idea, but its versioning system is specific to its language. We need this everywhere.

The payoff of semantically structured source code management isn’t just nicer code search — it changes the very physics of development. Merges can happen at the level of operations and graph relationships rather than line ranges. Conflicts become rarer, smaller, and more meaningful. Agents can ask for precise, dynamic slices instead of cloning and reparsing everything. Tools can ask for the view of the codebase they care about, and get just that back.

The codebase is the heart of development, and giving it real structure lets us start disentangling everything else.

Link to Tools become remoteToolsbecomeremote

Every dev tool just rummages through the whole machine until it finds what it wants.

A linter reads whatever files it likes. A dev server grabs its favorite port. A test runner opens sockets and reaches for env vars — none of them asking first, because the machine lets them. This is exactly why a tool that runs fine on your laptop mysteriously breaks in CI, or needs a full page of setup instructions to run anywhere new: its real inputs were never declared, only discovered by poking around the system. Most tools can’t actually describe what they depend on; they just know how to forage.

The alternative is to give a tool exactly what it needs, and nothing more. Instead of a whole machine, an invocation gets: an immutable snapshot of the code; opaque handles to the modules, configs, and dependencies it’s allowed to touch; a declared set of capabilities it may call; and a budget and trace ID to run under. It does its work and hands back its results instead of scattering them across the machine.

This splits cleanly in two: one side owns the source state, snapshots, and credentials and decides what any given call may see or change; the other just runs the tool. Move that tool to another machine and it behaves identically, because it was never reaching into its surroundings to begin with.

Bazel Remote Execution, BuildKit, Nix, devcontainers, Codespaces, BuildBuddy, and EngFlow all show pieces of this. But many still reproduce the whole machine, or are limited to builds rather than the entire development loop. What’s missing is a general contract every tool can speak — formatters, linters, compilers, test runners, bundlers, schema checkers, migration validators, preview builders, and whatever comes next.

Portability shouldn’t come from reproducing the machine; it should come from never needing one.

Link to Outputs become first-classOutputsbecomefirst-class

stdout is a cave wall that most tools still paint on.

Diagnostics are printed, test results are dumped, artifacts are written (somewhere?). Logs are mountains of text, screenshots are temp files, provenance is folklore. The caller scrapes what it can and hopes the format doesn’t change too much. It works, mostly, because a human is on the other end making sense of it all.

This no longer works once we start pulling the development environment apart. Every one of those outputs — diagnostics, suggested mutations, test results, artifacts, traces, provenance, audit and budget records — should come back as a typed object with a schema, not a blob to be parsed by optimism.

SARIF did this for static analysis; TAP did it for tests; SLSA and in-toto do it for build provenance. The next development environment needs that instinct everywhere. If a tool did work, the environment should know what happened — not by reading the tea leaves of console output, but because the tool returned typed evidence.

Link to Scripts become applicationsScriptsbecomeapplications

Most substantial repos eventually grow a shadow application under scripts/.

You might have a script to provision the test database. A script to create a release. A script to backfill production data. One to regenerate fixtures, and another to poke a flaky service. Then there are internal CLIs, package scripts, shell snippets in READMEs, one-off notebooks and runbooks, and pinned team messages with commands you should copy very carefully. This isn’t “just scripts”; it’s an operational application with a terrible user interface.

The fix isn’t a nicer script runner. It’s to treat that accidental application as a real one — to aggregate the scattered commands into a single, intentionally designed surface where every operation has a name, input structure, auth, config, logging, and an audit trail. The point is design: deciding, on purpose, which operations the system exposes and how they fit together, instead of letting them pile up, one shell script at a time.

What that surface contains is deeply context-specific — the operations a payments system needs aren’t the ones a design system needs, and no standard can define them for everyone. That’s exactly why it deserves deliberate design rather than quiet accumulation.

Centralizing these operations also lets you expose them however you need — a CLI, an HTTP API, MCP tools — so humans, editors, and agents all work against one control surface, with the same permissions and observability. The pile of scripts becomes a real application in its own right.

Link to UI becomes bespokeUIbecomesbespoke

The modern text editor is an immensely successful lowest common denominator.

Editors work, but the trouble is that real systems aren’t just text to edit, they’re full domains to inhabit: a personal site has posts, projects, drafts, photos, and deployments. A payments system has merchants, ledgers, events, reconciliation jobs, risk rules, migrations, dashboards, and incidents. A design system has components, tokens, examples, visual tests, package releases, and adoption maps. Why should all of that be flattened into files and tabs (and maybe a sidebar tree, if we’re feeling fancy)?

The development environment should be able to express and evolve a user interface specific to the codebase. Some of it may be designed by hand. Much of it can be derived from things the environment already knows: the semantic graph, the declared operations, the previewable resources. And agents can synthesize task-specific views on demand. The mechanics are an open design space; the point is deciding that a codebase deserves its own interface at all.

There’s rich prior art here too. Glamorous Toolkit and moldable development argue that tools should be cheaply shaped to the system being studied. JetBrains MPS and Intentional Programming explore projectional/domain-specific editing. Observable shows how documents, code, data, and interface can collapse into one domain-specific surface. Once development environments expose real contracts, these ideas finally get a substrate to build on.

The development interface should be as rich and specific as the domain it serves — shaped around the actual objects of the work, not flattened to fit a generic editor.

Link to Authority becomes explicitAuthoritybecomesexplicit

Today’s development environments grant power by accident.

If a process can run in a repo, it can probably read nearby files too. If it can write to disk, it can patch files it was never supposed to touch. If it can read env vars, it can probably see a secret or two, and if it can open the network, it can exfiltrate them. We’ve decided this is fine because the developer is “in control” of their own machine. That story doesn’t survive hosted tools, shared workspaces, third-party processors, and autonomous agents. Instead, authority should be declared, scoped, budgeted, and audited.

This only becomes possible because of the earlier shifts. Once every tool runs against a defined contract, and every operation is a declared control rather than a loose script, each scoped invocation becomes a seam where authority can be attached — or withheld. Defining the contracts didn’t just tame the chaos; it gave us somewhere to put permission.

A tool should not be able to spawn processes just because it is a process. A config file should not become executable authority by existing. A browser test should not imply open network egress. An agent tool should not receive secrets because a shell would have. This is one of the deepest architectural shifts: capabilities and authority are different things. A system can allow a tool to resolve dependencies but not run them, read config as data but not execute it as code, and suggest changes but not write them.

The development environment needs a security model finer than “on the same box.”

Link to Agents become ephemeralAgentsbecomeephemeral

Most agent systems still feel like a developer trapped inside a terminal session.

Today, an agent process stays alive while it waits for package installs, CI, a deploy, a browser check, a human approval, a network retry, or another agent. That’s the wrong way around; the durable thing should be the task state, not the agent process.

A development agent should be able to receive a task, inspect indexed code state, call declared controls and remote processors, and persist its decisions, artifacts, and evidence as it goes. And when it hits one of those inevitable waits, it should be able to suspend and resume later on an entirely different worker. It simply doesn’t need to be heavier than that.

Cloudflare Workers are a good mental model for the compute posture: short-lived, suspendable execution over persistent state. Temporal, Inngest, Restate, and LangGraph are all exploring durable execution in their own ways. Coding agents need that shape too.

Future codebases may have scores of simultaneous agents doing small pieces of work: updating generated fixtures, resolving semantic conflicts, testing migrations, reviewing changed routes, refreshing screenshots, trying repair strategies, or waiting for flaky external systems. Keeping all of those alive as standing processes is extravagant; keeping their state alive is the point.

Link to Performance becomes utilizationPerformancebecomesutilization

Most dev compute is a reserved machine watching itself do nothing.

Our current model scales by reserving a whole computer for work that mostly isn’t happening. A cloud coding task might get a dedicated sandbox for the better part of an hour, and it spends most of that time cloning the repo, restoring dependencies, starting services, and waiting for reasoning tokens. Only a thin slice of that reserved time is meaningful work.

Once we break up the monolithic dev environment, this all changes. The codebase is already indexed, branches are cheap snapshots rather than fresh clones, source arrives by semantic slice, and dependencies and processors are shared services that are already running. Tools run remotely against scoped inputs, and agents suspend while waiting instead of keeping the whole machine to themselves. The exact multipliers will vary greatly by codebase, but when you stop reserving idle machines and instead pool compute around actual activity, early benchmarking and back-of-napkin math suggest an order-of-magnitude increase in useful work. The real world is messier, but the shape of the waste isn’t subtle.

Admittedly, pulling the environment apart may cost some speed on individual tasks — and that’s okay. Raw speed matters most when a human’s single-threaded attention is stuck waiting on a build. But when countless agents work 24/7, with humans supervising asynchronously rather than blocked on any single result, the throughput across the fleet matters much more than the stopwatch on one task.

Today’s model scales by reserving computers. The new one scales by actually using them.

Link to Development becomes integrationDevelopmentbecomesintegration

CI exists because we don’t believe anything that happened on someone else’s box.

Continuous integration is institutionalized distrust: work doesn’t count until a blessed machine has redone it. That made sense when development meant a mutable checkout on a laptop. The integration system had to clone the repo again, install everything again, run the checks again, and anoint the result in a more official place.

But once tools and code no longer share a machine, the important unit isn’t a machine — it’s an immutable snapshot plus evidence produced by trusted services. If an agent runs a build, test suite, or migration check through approved processors against an exact snapshot, that result should be durable validation evidence. Later integration shouldn’t reflexively repeat the same work just to make it feel real.

CI doesn’t disappear; it stops being the place where work becomes trustworthy. Integration becomes a matter of checking evidence: was this exact snapshot validated, by a service we trust, under rules we accept?

Development work should arrive carrying its own proof.


Put together, I think we’ll end up with something like this:

Source control stores semantic operations and indexes every branch; Codebase hosts expose immutable snapshots, handles, collections, resources, and writes; Processors run remotely against scoped capabilities; Validation evidence is recorded against exact snapshots and checked by policy; Operational controls are hosted applications projected as tools; Workspaces are domain-specific experiences; Agents are ephemeral workers over durable task state; Compute is pooled around active work instead of reserved around idle sessions; Authority is always explicit.

This is (probably?) not a weekend refactor of software development.

Perhaps it’s not really a refactor at all. It’s more like giving the most important platform in our field its first deliberate design. Fortunately, it doesn’t have to happen in one motion: we can extract the hidden contracts one by one and prove each boundary is useful on its own, so the architecture sketched here gets earned piece by piece rather than imposed all at once.

None of this requires deleting files, shells, terminals, local checkouts, or generic editors. Those surfaces are how we migrate, how humans inspect edge cases, and how existing tools keep working while the deeper architecture changes underneath them. What changes is their standing: each surface becomes a projection of something deeper — files of semantic operations, CLIs of hosted controls, editors of domain-specific workspaces, local machines of remote codebase state. They stick around exactly as long as they earn their keep — some may quietly fade, others may outlive us all. The question is not whether we keep the old surfaces, but which layer is authoritative.

Many pieces already exist in partial form. Language servers proved that editors and tools can meet at a protocol boundary. Remote build systems showed that execution can move away from the laptop. Code indexing systems demonstrated that source can be queried as a graph. Internal tool platforms revealed that scripts want schemas and UIs. What’s missing is the compositional view.

So, what exactly are we building? What’s on the TODO: list?

  • Codebase server: This one is almost certainly the hardest, both conceptually and technologically. Figuring out how to represent and control a codebase, both correctly and efficiently, without a filesystem substrate requires rethinking many decades of deep assumptions. Early prototyping suggests this is less scary than it may first sound, but it’s neither trivial nor guaranteed.
  • Processor servers: The primary proof that we got the codebase server right is that dev tools actually work against it. There’s a very, very long list of existing tools that would need to be taught to speak a new protocol, and any single one of them could expose flaws in the codebase host’s contract design. Much of the implementation work for these can likely be agentically automated, but that still doesn’t make it easy.
  • Broker server: As soon as we start pulling things apart, we’ll want to start connecting them back together. Service and capability discovery is a pretty well understood problem, so this one isn’t conceptually intimidating. It does, however, require settling on a contract that nearly every service we’re talking about will agree to, which is its own flavor of challenging. But we’ve written protocol specs before, so we’ll add this one to the list.
  • Custom interfaces: The simple version of this is “everyone starts from scratch” with a boilerplate web app project and builds something custom. This might be fine for at least some contexts, but I suspect it won’t scale. There’s likely shared infrastructure necessary to make per-codebase interfaces coherent and trustworthy. However, because this is the top layer (nothing depends on it, other than humans), universal standardization isn’t required.
  • Tool servers: Moving scattered utility scripts into a proper application isn’t conceptually difficult, it’s mostly just work. The trick here will be to figure out best practices and build shared infrastructure that makes this simple to implement.
  • Agent server: By the time we have the above infrastructure, this one’s pretty straightforward. The agent server might not need to be a blast-proof chamber where arbitrary generated code detonates, because tools are remote and authority is scoped elsewhere. It mostly just needs to run many agentic loops as efficiently as possible, with some basic session state management.
  • Ledger and policy servers: The CI-shaped part will probably be the last thing to happen — we’re not doing that until we fully trust everything else. But this is less about “replacing CI” than about shifting integration from rerunning work to admitting evidence. These components are mostly about governance logistics, and that’s another well-trodden topic in software systems. It will take time to get the spec right and harden the systems, but again, it’s not fundamentally challenging.

That list isn’t complete — it leaves out identity and observability, for example — but it’s plenty to get us started.

We should stop asking for a better dev box. The development machine was a compression artifact: one computer standing in for source state, compute, tools, scripts, secrets, UI, and human attention — then copied into a second machine (CI) because we couldn’t trust the first. It worked because a human was always there to manage the mess. That’s no longer the world we need to build for.

If it’s time to rethink everything (and indeed, it is), the systems we work in are no exception. Let’s rearchitect the development environment. (If you want to build toward this together, let’s talk.)