(Heads up: the middle section of the video gets into some pretty heavy territory near the middle.)
Early on, there’s some important conversation about rebuilding and repairing communities that have been shattered by a system that deliberately pits us against one another.
All hierarchies of power desire to keep us separate, competing, and individualistic, because because we’re easier to exploit that way.
Dismiss the idea that your task is only to build councils of like-minded individuals. You have to start from a presumption that those around you are salvageable, until the time comes that they prove to you that they’re not.
It’s by this rebuilding of our shattered communities that we might rediscover solidarity with our fellow humans.
I’ve definitely noticed how much the last few years have really intensified this tendency.
There’s been so much fuckery from so many people for so long that I’m quick to cut people off, and have a very low tolerance for anyone still holding conservative values.
But that’s exactly what the rich and powerful want.
This was a good reminder that the kind of anarchist community I want to live in has people with lots of different beliefs and values, and so long as they don’t infringe on other peoples’ right to a rich and fulfilling life, that’s not just OK, but desirable.
A few questions about Vertical Slices come up again and again, and they’re good ones.
If a slice cuts through all the layers, do we get a table per slice?
If I’ve split the application into seven areas, are those seven bounded contexts, or seven slices of one? And does the answer change what they’re allowed to know about each other?
How can a “verify order” slice check what a “register order” slice wrote, if the two aren’t supposed to know about each other?
Where does the fetching code live when one screen needs data owned by another module, and the dependency rules block every place we could put it?
Should a UI component know about the API call a neighbouring component makes?
For me, these are all versions of one question: what does a slice do when it needs something from the outside world?
Calling every area of an application a bounded context sets the bar for separation as high as it can go, because contexts are genuinely meant to be autonomous. Once you’ve done that, connecting any two of them looks like a violation, and the other questions have no legal answer.
The assumption doing the damage rarely gets stated because it feels too obvious: a slice ought to be self-contained, so needing something from elsewhere means the cut was wrong. I held it myself for a while.
Vocabulary is where this gets tangled, so let me start there.
Slice, module, context
Three words that get used interchangeably, and I think a lot of the pain comes from that.
A vertical slice is one piece of functionality, cut through the whole application. For me, a slice is more a function than an entity. “Verify a transport order” is a slice. It has a way in, some business logic, and whatever it reads and writes. If you’re thinking of it as a thing with a lifecycle, you’re probably thinking of an entity, which is a different concept that lives within the slice’s reach rather than being the slice.
A module is a logical grouping of slices. Orders is a module. It holds registering an order, verifying it, confirming it, and listing what’s pending. The grouping is a judgement call, and the criterion I use is what changes together.
A bounded context, in DDD terms, is a linguistic barrier. It’s a set of functionality that the business uses the same vocabulary for, distinct from other contexts. In a given context, a word always means the same thing. Across contexts, the same word can mean something else entirely.
A frontend and a backend aren’t two bounded contexts; they’re two deployment targets. You can have one deployment for multiple contexts, and a single bounded context with multiple deployments. If a frontend feature is part of the bounded context and has backing WebAPI, then the business should use the same words across both. Two product listings that differ in which fields they show aren’t two contexts either, since nothing about the vocabulary changes between them. Draw the line in either place, and you get the terminology without the boundary (or just a pure technical boundary), and then wonder why the “contexts” need constant coordination.
Seven features of one application are almost always slices, or at most modules, sitting inside a single context. That’s good news, because it means they were never obliged to be autonomous.
I’ll be honest: I don’t love the term “bounded context”. It’s imprecise in the original books and even more misleading in practice, because most people encounter it third-hand and take it to mean “a big folder we agreed to keep apart”. If the word causes arguments on your team, drop it and talk about which functionalities share a vocabulary. That usually works better.
Labels aside, what I do in practice is start from the functionalities we have to deliver, group them logically into components, and check that grouping against the UX map. Technological splits (a layer for data access, a layer for services, one for the frontend and one for the backend) have always ended badly in my projects. Business grouping usually held up longer.
Slices boundaries in practice
Let’s use the transport order. It gets registered, then verified, then confirmed, then settled. Verification means checking that the contractor and the driver actually exist in the carrier’s own system, which is an external API we don’t control.
It’s tempting to model this as one orders row with a status column that moves registered → verified → confirmed, updated through a generic endpoint. That’s how it usually starts, and the trouble begins there, because the endpoint that changes status also changes the pickup address and the contact phone. The system records that the order is verified. It doesn’t record that anybody verified it.
Greg Young made this argument better than I will in Task-Based UI: when the client posts data-centric structures back and forth, the domain has no verbs, and the user’s intent is lost on the way in. His point is that the client should tell the server to do something, so that the intent becomes the exact task for the backend expressed by the message we sent (rather than being inferred from a combination of fields mixed with the current state).
Naming the operation is also what gives you something to slice along. If every operation is “update the order”, you have one feature and nothing to divide. Once you have VerifyOrder, ConfirmOrder, RejectOrder, you have folders, and each folder is named the way the business names the operation. This holds for plain CRUD systems too. The name is the value, and the underlying implementation can be a single UPDATE.
Which leaves the question I started with. Verification needs the carrier’s system, and confirming needs to know what verification established. Where does that go?
The shape I use is a handler taking two arguments: its dependencies, then the message.
The first parameter is dependencies, and I think about it the same way I think about React props. The component declares what it expects; whoever renders it decides what to pass. Same here: the handler declares what it expects, and it has no opinion about where those functions come from: the same module, a different module, an HTTP client, or a stub.
The business logic underneath takes everything as data and returns a decision:
// orders/verifying-order/verifyOrder.ts// Business logicexportconst verifyOrder =(
command: VerifyOrder &{
contractor: ContractorStatus;
driver: DriverStatus;},
order: Order,): Order =>{if(order.status !=='Registered')thrownewError(`Cannot verify an order in ${order.status} state`);if(command.contractor !=='Active')thrownewError(`Contractor is ${command.contractor}`);if(command.driver !=='Licensed')thrownewError(`Driver licence is ${command.driver}`);return{...order,
status:'Verified',
verifiedBy: command.verifiedBy,
verifiedAt: command.now,};};
Nothing here is fetched, so testing it means passing values in and checking what comes out. No mocks, no container, no database.
There’s no ITransportManagementSystem interface here with fifteen methods. Pragmatically, you can pull the whole thing in, and plenty of codebases do, but I’ve come to prefer narrowing it: two functions, three cases each, defined in this folder, in this slice’s language.
The carrier’s system almost certainly has a richer model of a contractor than the three cases, including credit terms, insurance validity, certificate expiry dates, and territorial permissions. Verification only needs to know whether this order can proceed. Declaring the narrow type keeps the slice in its own vocabulary rather than importing someone else’s, which is the bounded-context idea applied on a much smaller scale.
Duck typing and dependencies
Nothing in the codebase declares that it implements CheckContractor. There’s no implements clause anywhere. Any function of a compatible shape satisfies it. That means the type can live with the consumer, the slice that needs it, rather than with whoever ends up providing it.
So you can write the need, the handler, and the tests before anyone has decided who will serve it. Whether checkContractor becomes an HTTP call, a query against a table we replicate nightly, or a function returning 'Active' for the first three weeks stays open. When an external shape doesn’t match, you fit it or remap it at the point of supply, and neither side changes.
Golang bets its whole dependency story on this. Interfaces are satisfied implicitly, and the convention is that the consumer declares the interface it needs, sized to its use. Hence the proverb about the bigger interface being the weaker abstraction. io.Reader is one method, and half the standard library composes through it.
Structural typing gives us that in TypeScript, and I think it’s underused by people arriving from C# and Java, where the habit is to define a nominal interface and hand it around.
So where do the dependencies come from?
Somewhere near the entry point, you create the real things once, build the small functions the handlers asked for, and pass them in:
That mapping function is where two vocabularies meet, and I like having exactly one place where that happens. Everything the carrier knows about a contractor collapses into three cases, in a file you can read in ten seconds.
That’s the mechanism: partial application, done by hand, in a file whose job is to know about everything so that nothing else has to. No container, no registration, no lifetime scopes. In a monorepo, this file lives in apps/, and packages/ holds slices that declare but never resolve. If you’re using Nx, this is the composition point its boundary rules exist to protect. In other environments, where Dependency Injection Containers are out of the box, you can still use the same way; in .NET, you can use the same pattern by injecting dependencies explicitly even to methods.
The tests use the same shape:
const contractor =(status: ContractorStatus): CheckContractor =>()=>Promise.resolve(status);test('refuses to verify an order for a suspended contractor',async()=>{const store =inMemoryOrderStore([registeredOrder]);awaitexpect(verifyOrderHandler({...store,...carrierStubs, checkContractor:contractor('Suspended')},
verifyCommand,),).rejects.toThrow('Contractor is Suspended');});
There’s no mocking framework here, because there’s nothing to mock. You can pass an in-memory implementation, a stub, or a real client pointed at a sandbox. The handler can’t tell the difference.
Everything outside the slice is external
From inside a slice, there’s one category of thing: external. Another slice next door, the parent module, a different module, a third-party API, the database. All the same. The slice states a function type and doesn’t ask where the implementation comes from.
That keeps the slice movable. Because it defines its own dependencies, changing the logical grouping later is a matter of what you inject, not a rewrite. If verification turns out to belong in its own module, or gets extracted into a service, the handler stays as it is. Testing in isolation comes out of the same property, as the test above shows, and I get it as a side effect rather than designing for it.
None of this is about hiding the coupling. For me, independence isn’t a value in itself. I’d rather know the connections I need to have and be able to look at the code to see what it depends on and what it does. Hidden dependencies are still there, only harder to find. What I’m optimising for is cohesion, and explicit dependencies serve that.
What about the module’s public API?
If you’ve read my Architecture Weekly piece on VSA, you’ll have seen me recommend an api.ts per module, exposing what’s public and hiding the rest. That may seem to contradict what I’ve just described.
They point in opposite directions across the same boundary. api.ts is what a module offers: the surface it’s willing to support and that the module owns. CheckContractor is what a slice asks for: a need, owned by the consumer, expressed in the consumer’s terms.
You want both, because they protect against different things. Without the module API, everything is reachable, and any internal change can break a caller you didn’t know you had. Without consumer-declared needs, the consumer is coupled to the shape of whatever the provider decided to expose, including the parts it never calls.
The composition root is where the two meet. It takes what the module offers and adapts it to what the slice asked for. That adapter is a few lines, it lives in one place, and it’s the only code that knows both vocabularies.
Cycles stop being a problem. Orders needs contractor standing. Pricing needs order history to work out volume discounts for the same contractor. That’s a genuine mutual need, and if both modules import each other, you have a cycle. The compiler may tolerate it. Your boundary rules probably won’t. This is the situation where the tooling forbids two modules at the same layer from referencing each other, and it’s easy to conclude from that the design must be wrong.
The usual escape is to extract the shared parts into a common module. That works twice; then the common module becomes the place everything ambiguous lands, and changing it means changing everything.
But there’s no cycle if neither module imports the other. Orders declares CheckContractor. Pricing declares its own:
Two fields, because that’s what a volume discount calculation uses. The composition root supplies a function that queries orders and maps the result down to that shape.
Most module-level cycles I’ve run into were a shared concept whose owner hadn’t been decided yet. Declaring narrow needs lets you defer that decision rather than resolve it early and wrong. If it later turns out that contractor standing genuinely belongs in one place with a single definition, you’ll know more by then, and moving it is cheap because only the adapters point to it.
If your dependency rules forbid the arrangement your domain wants, it’s worth checking whether they’re describing your design or your import graph. Rules that block a module from reaching another module are useful. Rules that block it because two files sit at the same “layer” are enforcing a layering you may have already outgrown.
Two entry points, one feature
What if the same operation can be triggered two ways?
An order gets confirmed by a dispatcher through the UI. It also gets confirmed automatically when the carrier’s system reports the assignment accepted. Same rule, different trigger, different surrounding logic.
Both files go in confirming-order/, side by side. I showed this pattern in Vertical Slices in practice using an API endpoint and an external booking event that share a single command handler. With dependencies declared per handler, the event-triggered one looks like this:
The two handlers don’t have the same dependencies. This one has no carrier check at all, since the event came from the carrier and we already have the answer. The dispatcher-triggered version loads more, because it starts with less. The business logic is shared; the application logic isn’t, and each entry point declares what it actually uses.
The same applies to what you reuse across slices. I’d reuse a policy or a calculator, say a pure function like settlementFor(order, tariff) used by both confirmation and settlement, before I’d reuse a whole handler. The pure function is a function of its arguments and cheap to share. Handlers differ in what they load, check and record, and that’s the part that tends to change.
Vertical Slices and Database
Back to the table per slice. Three separate things get conflated in that question.
Business logic goes per entity or aggregate. The rules about what states an order can be in and which transitions are legal belong to the order. One place, and every slice that decides about an order goes through it. Slices don’t each get a private notion of what an order is.
Read models go per query. This is where a table per feature is right. The dispatcher’s pending-orders board needs order number, contractor, pickup window and verification state, sorted by pickup time. Build a table that answers that. The settlement report needs something else and gets its own. Resist making a single query serve five screens by expanding columns.
Database schemas go per module. One for orders, one for pricing. A slice is a feature, not a persistence boundary. A schema per slice gives you migrations that correspond to nothing, and joins across five schemas to draw a single screen.
You can do all of this with one status column and an ORM. If you want the four distinct operations that currently collapse into status = 'cancelled' to stay distinguishable, appending them as facts is what Event Sourcing offers, and it composes well with this. Nothing above depends on it.
Vertical Slices and Frontend
Don’t force a 1:1 mapping between the UI and the backend. One screen is routinely composed of data gathered from several modules, and one user action can trigger operations in several. That mismatch is normal. GraphQL exists partly as an attempt to solve it, whatever you make of that as a solution.
A dispatcher board showing pending orders, contractor status and a confirm button touches three slices across two modules. Two reasonable arrangements:
Compose at the page. The page fetches from several endpoints and arranges the results, holding no business logic of its own. Components receive what they need as props and know nothing about where they came from, for the same reason that handlers take dependencies as arguments. A component that takes props is easy to move and easy to feed with a different implementation.
Or write a backend-for-frontend. If the screen needs it in one request, make a slice whose job is to serve that screen, depending on the others and stitching them together. It’s still a slice; it’s named after a screen because that’s honestly what it is.
Which one depends on how much the round-trip costs you and how stable the screen is. Both beat growing one endpoint until it serves every screen you have.
The backend usually knows which operations are currently available for a given order, and the frontend is often re-deriving that from status fields. Returning the available actions with the data keeps that decision in one place. That’s the part of HATEOAS I find useful, without the rest of the ceremony.
Splitting the frontend by kind of thing tends to look suspicious to me. Two listings that differ only in which fields they show are usually one feature with a parameter. Splitting by market or country is more often real, because it genuinely is a different application with different rules. It’s hard to judge from a description; what I’d do is map the functionalities first and see which ones actually change together.
TLDR
The question I started with was what a slice does when it needs something it doesn’t own. My answer is that it declares the need and leaves it to something else to decide where it comes from.
Which comes down to:
Everything outside the slice is external, whether it’s the next folder or another system.
Declare narrow function types where they’re used, in your own vocabulary, rather than importing a wide interface.
Compose by passing functions in, in one file, near the entry point.
A module’s public API and a slice’s declared need are different directions of the same boundary, and the composition root adapts between them.
Reuse policies and calculators; let handlers duplicate.
Business logic per entity, read model per query, schema per module.
Don’t expect the frontend to mirror the backend.
None of this needs a framework, a container or a particular database. It’s more about a convention applied consistently.
And your first grouping will still be wrong somewhere. Mine usually is. That’s fine, as long as being wrong stays cheap, which is the argument for removability over maintainability and for keeping the couplings visible rather than tucked away. A slice that states its dependencies is one you can move.
And the cherry on top: this approach helps LLM agents too. Everything a feature needs sits in one folder, with a handful of function types crossing the boundary. To change how verification works, you open verifying-order. The command, the rules, both entry points and the tests are in it.
That was always the argument for grouping changes together: it reduces how much a person has to hold in their head. The same property determines how much can fit in a context window and how much of a codebase an agent has to read before it can safely change one behaviour. A layered structure that needs five folders touched for one feature costs an agent the same way it costs us, faster.
So it’s an old argument that happens to have got more valuable.
Cheers!
Oskar
p.s. Ukraine is still under brutal Russian invasion. A lot of Ukrainian people are hurt, without shelter and need help. You can help in various ways, for instance, directly helping refugees, spreading awareness, putting pressure on your local government or companies. You can also support Ukraine by donating e.g. to Red Cross, Ukraine humanitarian organisation or donate Ambulances for Ukraine.
Disclaimer: This post was originally published on Azure with AJ and has been reproduced here with permission. You can find the original post here.
I did not want to like the GitHub Copilot app. I had VS Code tuned exactly how I wanted it, extensions curated over years, keybindings in muscle memory, and a colour theme I will defend to the death. Handing that over for a new desktop app felt like swapping a lightsaber I had built myself for one someone handed me in a shop.
Fast forward and it is open on my machine every single day, sitting alongside VS Code rather than replacing it. VS Code is still where I go for heavy code development, the hands on work where I want the folder tree, the extensions and the debugger. The Copilot app is where I direct the work.
This is not a scorecard, it is my view on why the GitHub Copilot app is now a valid place to do serious work, the good, the awkward and the genuinely annoying. Everything factual here comes from GitHub’s own documentation, everything opinionated is mine.
What the GitHub Copilot app actually is
Before the feelings, the facts. The GitHub Copilot app is a desktop application purpose built for agent driven development, available on macOS, Windows and Linux, and available across all Copilot plans. It is built on GitHub Copilot CLI and integrates natively with GitHub, so repositories, branches, issues, pull requests and CI results work out of the box.
The important word in that description is agents, not editor. GitHub is explicit that it exists so you can direct multiple agents across parallel workstreams instead of context switching between terminal, IDE and browser tabs. Each session runs in its own isolated workspace with a dedicated git worktree and branch, and you can pick where a session runs, a new working tree, your local repository, or a cloud sandbox.
Early on it was mild frustration. I kept asking myself the obvious question, why would I use this when VS Code already has Copilot in it?
The answer was not obvious, because I was using the app wrong. I started with chat, treating it as a fancier chat window, which is the least interesting thing it does. Then I kicked off real agentic work and the discomfort arrived properly. No folder tree down the left. No extensions bar. No terminal sitting where my eyes expect it. My hands kept reaching for shortcuts that were not there.
That reaction is worth naming, because it is not a product flaw, it is a model mismatch. An IDE optimises for you writing lines. The Copilot app optimises for you directing work and reviewing outcomes. The sidebar tells the story, My work, Automations, Search and Sessions. Not a file explorer in sight, because files are what the agent is dealing with, not you.
I was looking for a cockpit and had been handed the war room on Yavin 4. Once I stopped mourning my folder tree, things got interesting fast.
The turning point: from curiosity to daily habit
The shift did not happen in one dramatic moment. It happened over a handful of real use cases, one after another, a refactor here, a documentation sync there, adding a new skill in a repo on a Friday afternoon that I could not be bothered branching for manually. Nothing individually convinced me, but the tally added up quickly, and I noticed I was reaching for the app before I had consciously decided to.
Part of what made experimenting low risk is session modes. You choose how much rope the agent gets, and you can change it mid flight:
Interactive, the agent suggests changes and waits for your input.
Plan, the agent proposes a plan you approve before it executes.
Autopilot, the agent writes code, runs tests and iterates on its own.
I will be upfront that Plan mode has not been my own habit, my sessions have mostly lived in Interactive and Autopilot. But it is a genuinely useful option if you want a checkpoint before an agent starts touching your repository, and combined with a model picker and reasoning effort control per session, the range of control on offer is well thought out regardless of which mode you settle on.
What changed is that it slowly became essential for real development work rather than something I dipped into occasionally. When I have three streams of work in flight across two repositories, this is where I sit. When one of those streams needs me elbow deep in the code itself, I drop into VS Code, do the work properly, then come back.
Automations are the sleeper feature
If I had to pick one thing that moved the app from “interesting” to “essential”, it is automations.
Automations let you save recurring agent tasks and run them on a schedule or on demand. In the app you get an Automations tab in the sidebar, and each automation shows its name, schedule, associated repository and last run status. There are two flavours, local automations that run from your environment, and cloud automations that run in a cloud environment so they still fire when your machine is off.
Triggers are refreshingly simple:
Manual, run it whenever you want with the play button on its card.
On a schedule, hourly, daily or weekly.
When an issue is created, with an optional search query filter so you only catch the issues you care about.
For cloud automations you also select the tools Copilot may use, such as pushing changes, updating issue labels or creating a pull request. Selecting only what the task needs is the same least privilege discipline we apply everywhere else, and it is refreshing to see it as a first class dropdown rather than a buried policy.
My favourite detail is the smallest one. While troubleshooting I was able to schedule a retry for 24 hours later while I was waiting for a system change to propagate, meaning one off jobs can get saved easily rather than retyped from scratch every time I want to repeat them.
The other pleasant surprise was stumbling onto an inbox style view in the app that surfaces reminders and nudges me to run something when it is actually relevant, rather than leaving automations to fire blind on a timer and hoping I remember to check the results. It is a small addition, but it is the difference between an automation running while I am not looking and one that taps me on the shoulder at the right moment.
Multiple accounts, or why my cross org life got easier
Working across a personal account and multiple client organisations has always meant an authentication tax. Sign out, sign in, re authorise, forget which identity you were in, push to the wrong remote, feel shame.
Being able to work across accounts and set the identity per repository and session removes what has honestly been a barrier to entry for years. In practice it is the difference between picking up a cross org task immediately and putting it off until I have the energy for the ceremony.
The rough edges
With that said, here is what still irritates me. Both feel like maturity problems rather than design problems.
Customisation still pulls you out of the UI. The docs say you can add and manage agent skills and MCP servers in app settings, and anything already configured for your repositories or Copilot CLI is picked up automatically. That is true, and there is a catalogue of popular MCP servers. But the moment you go beyond the catalogue and need to provide in depth customisations , you are back in local configuration files, restarting things and guessing why a server did not appear. For an app whose whole pitch is “stay in one place”, the customisation path is the one journey that keeps sending me elsewhere.
No terminal until a session exists. Sessions own the workspace, which makes sense architecturally, since each one gets its own worktree. It also means the terminal is not there when you first open the session. If your instinct is to poke around a session before deciding what to do, that instinct is temporarily homeless. Quick chats help for questions, because they open a conversation without creating a branch or worktree, but they are not a shell.
Neither is a deal breaker. Both are the kind of thing I expect to read about in a changelog within a couple of releases.
Credits and common sense
One practical note. Agent sessions consume AI credits, and GitHub publishes sensible guidance on optimising usage, match model capability to task complexity, use Plan mode to validate scope before burning effort, use quick chats for early exploration, and start a fresh session when you switch tasks so you are not dragging irrelevant context along.
Treat autonomous runs like cloud spend. Start narrow, watch the usage, expand what clearly pays for itself.
Conclusion: who should actually switch
The GitHub Copilot app did not replace my IDE and I no longer expect it to. VS Code is still my lightsaber for heavy code development. The Copilot app is the command deck, and it replaced the orchestration layer that used to live in my head, spread across terminal tabs, browser windows and half remembered intentions.
I would recommend it to you if:
You regularly run more than one stream of work at a time.
You live across multiple organisations or accounts and are tired of the sign in shuffle.
You have recurring repository chores that would happily run on a schedule.
You are comfortable directing and reviewing rather than typing every line.
I would hold off if you are mostly doing focused single threaded work in one repository, or if your workflow depends heavily on IDE extensions. These are not the droids you are looking for, and the app is not trying to win that fight anyway. Keep VS Code for the deep code work and let the app handle everything around it.
Start small. Install it, connect one repository, run one Plan mode session on a real issue, then save one automation. That is a lunch break’s worth of effort, and it is enough to know whether the model fits how you work.
Have you given the GitHub Copilot app a proper go, or are you still loyal to your IDE? Tell me what won you over, or what sent you back, in the comments.
This week’s collection highlights several key advancements in Azure performance, the nuances of orchestrating multiple AI agents, and critical updates to NuGet security. These pieces offer practical insights for anyone looking to streamline their development workflow while maintaining a more secure infrastructure.
Cloud
Azure Service Bus: Count your filters (Daniel Marbach) - Great deep post about performance. The architecture and number of filters can and will greatly affect the performance.
The following article originally appeared on Tim O’Brien’s Medium page and is being republished here with the author’s permission.
At some point, the software “Security” industry stopped talking about stopping threats and started talking about detecting them: detection windows, response times, mean time to remediate. It’s not offense or prevention; it’s damage control. There’s a movie scene that captures what that sounds like, and you’re going to name the film before I finish describing it.
An underground base on a frozen planet. The enemy knows exactly where it is. Massive mechanical walkers—walking tanks the size of buildings—are advancing across the ice. The defenses can’t stop them. The people inside aren’t trying to fight back. They’re frantically trying to get a broken ship working so they can just escape—not win, not hold the line, just get out before something catastrophic and unstoppable reaches the door.
The whole opening is just people preparing. Rushing. Running checks on equipment that isn’t ready, coordinating defenses that won’t hold, buying time against something too large and too fast to stop. Nobody’s planning a counterattack.
The entire operation is: slow it down long enough to get out.
Securing the Base (Image Assist from Anthropic)
The Empire Strikes Back
That’s what the conversation around InfoSec sounds like right now. The base is under attack. The walkers are AI-generated vulnerabilities, automated exploit chains, and speed that no human team can match. The framing has shifted from defending the perimeter to just getting the ship started. Not winning, just getting out.
Go back and watch that opening sequence carefully. There are hundreds of faceless Rebel troopers in that scene—no names, no lines worth remembering—scrambling to hold the perimeter, buy time, absorb the blow. Some of them continue to fight. But maybe some already understand that the base is lost.
Han is out on the ice looking for Luke. Leia is already on the transport, making sure the mission survives. The main characters aren’t defending the base. They’ve concluded the only way to answer the threat is to move. Most of the conversation around AI right now sounds like those faceless troopers continuing to defend: fortify what’s there, slow the walkers down, hold long enough for something to change. A few people are thinking like Han. They’re not buying another vulnerability scanner from a vendor. They’re asking whether there’s a different way off the planet entirely.
Here’s the disconnect: most people focused on “Security” have spent decades being handed a finished base and then being asked to defend it. They weren’t involved in the architecture or approach that application developers have been using.
This new application uses Node.js—go defend it.
We’re using a new relational database because the architect wanted to—go defend it.
Our developers decided to start using an LLM. Can you secure it? Thanks.
In many cases, people responsible for security are not defining architectures as much as they are catching up. And as “developers” start to generate more code in a day than was possible in a month or a year, it’s becoming increasingly unrealistic to think of security as an afterthought.
When security is just a support team for software engineers, that’s building a base that might be indefensible.
The shift that actually matters isn’t a better scanner or a faster response team. It’s security people in the room when people are writing the prompts, when agents are assembling the dependency list, and when the basic system prompts are defining the authentication system—before any of those systems is in production. Not reviewing the finished base.
Security needs to be involved before anyone even starts to prompt a system’s creation.
“Machine speed” has become a conference catchphrase, which usually means it needs translation. Here’s what it actually looks like, pointed at you:
A network of agents found a zero-day in FFmpeg and didn’t announce it anywhere, just filed it internally.
A second agent scraped your team’s LinkedIn and X and noted who’s in Cancun next week.
A third logged your nightly load balancer latency blip as a cover for an attack.
A fourth studied your last three incident reports and estimated a 30-minute detection window.
The whole operation, including reconnaissance, timing, and coordination, ran in seconds. What previously required a dedicated red team and weeks of planning is now background processing that runs continuously, waiting for the right moment.
Five years ago, you would have tasked a room of scary-looking security people with profiling a target, capturing latency data, and maybe holding several meetings to discuss what they found. Today, the coordination I outlined in the previous paragraph might take a few minutes on a network of interconnected Nanobot, Picobot, Hermes, or OpenClaw agents that gather data and then update a shared memory system, and the decision on when and how to attack would be made by another agent that was granted permission to coordinate the attack across a distributed network of agents.
Quick note: If you have anything to do with running a website, stop posting about your vacation plans.
Here’s what the conversation keeps missing: AI isn’t the real problem, and this problem isn’t necessarily new. The problem is that we’ve been building bases that were always going to need to be evacuated. The problem is that security is rarely involved in selecting a tech stack, and because that tech stack selection is frequently automated with AI, there’s no predicting the mess that’s being thrown over the wall.
The response to AI-accelerated attacks is almost entirely defensive. Tighten npm’s signing requirements. Fund the Maven repository. Sign up to support Akrites with the Linux Foundation. Add another scanner to the pipeline. These aren’t wrong. They’re just not enough.
These are important projects, and security groups should sign up to support them, but the real transformation that needs to happen is that more people in security need to get involved in software creation. What this looks like is having an opinion on React, Vite, Tomcat, Node.js, databases. It means jumping in and affecting some of the basic decisions that these agents are going to use before they deliver vulnerable software.
Most of the industry is still shopping for scanners. Most people in security are still “reviewing” software in a process that assumes it takes weeks or months to write.