July 14, 2026
Real Engineers Dig With Their Bare Hands
Yes, Father, I have sinned. I let a machine write the boring parts. No, I am not vibe coding. There is a difference, and the difference is two weeks of rules, a clear architecture, automated checks, and knowing exactly when to take the shovel back.
Two neighbors stop in front of a construction site and look through the fence. Inside, five workers are using two enormous machines to finish digging a massive hole. One neighbor shakes his head. “Fifty people could have dug that with shovels. Everyone would have had a job.” The other looks at him and says, “Why stop at fifty? A thousand people could have dug it with their bare hands.”
My previous post about working with AI generated a longer discussion than I expected in HN, so I want to explain what I have actually automated in my engineering workflow. This includes Git, code generation, project maintenance, architecture checks, migrations, testing, and plenty of other repetitive work that engineers apparently enjoy pretending is sacred. Before getting into the practical part, though, I need to address the predictable objections:
You are vibe coding.
You are AI-pilled.
You will eventually forget how software works.
These objections argue against a point I am not making.
I am not saying that learning how things work under the hood is a waste of time. Quite the opposite. That knowledge is precisely what allows me to automate safely, inspect the result, and recognize when an agent has done something stupid. Delegating grunt work is not the same as delegating judgment. Running formatters, checking architectural boundaries, finding empty files, detecting unused dependencies, and preparing a migration plan are mostly mechanical tasks. Determining whether the generated code is correct, whether the abstraction makes sense, whether the architecture is appropriate, and whether the system should exist in the first place still requires an engineer.
The machine does not assume responsibility. I do.
We have always automated engineering work
When I was younger, I uploaded files manually through FTP and SSH. I edited code in text editors with no syntax highlighting, no autocomplete, no inline warnings, and no useful feedback until compilation failed. Sometimes the feedback arrived even later, when the system broke in production. Great times. Very character-building. To reduce human error (or at least give it fewer opportunities to express itself), I remember using Teleport Pro and WinHTTrack to crawl our applications and find broken links. Then text editors acquired syntax highlighting, which felt almost unfair. IDEs started warning us before compilation. Autocomplete became normal. Suddenly, the computer was helping instead of silently watching us make mistakes.
I replaced repeated uploads with rsync, wrote shell scripts, and eventually adopted version control, automated tests, CI/CD pipelines, infrastructure as code, dependency bots, static analysis, and increasingly sophisticated deployment systems. Each new tool eliminated another task that developers had previously considered a sacred manual ritual. None of those tools made knowledge about servers, networking, permissions, failures, or rollbacks unnecessary. They simply removed repetitive execution, caught mistakes earlier, and reduced the number of Friday evenings spent discovering that someone had uploaded the wrong folder to production.
AI agents feel like the next step in the same progression.
I am not outsourcing understanding. I am automating execution and catching errors closer to where they are introduced and writing boilerplate code. In any case,engineers still need to decide whether the code is correct, the abstraction makes sense, the architecture fits, and the system should exist at all. We have used graphical Git clients, IDE refactoring tools, and autocomplete for years without declaring that engineering is dead because someone clicked a button. But as my grandfather said to me multiple times, “convenience always wins over nostalgia”, especially when it also prevents mistakes.
AI agents are another interface
Working with an agent is not fundamentally different from using another high-level interface over a complicated system. It is a new kind of GUI, although sometimes it is closer to a NOGUI because the interaction happens through language rather than menus. The interface is more powerful, but the principle is familiar. You describe the intended result. The tool performs a collection of lower-level operations, write boilerplate code for you. You inspect what it did. You approve, reject, or correct the result. The important difference is that agents can operate across a much larger surface area. They can read a repository, follow instructions, modify several components, run tests, inspect failures, and iterate. That does not make them infallible. It makes good instructions and verification much more important.
We are also past the stage where agents only generate tiny CRUD applications for demos. Anthropic recently described an experiment in which 16 parallel agents produced a Rust-based C compiler from scratch. Over almost 2,000 Claude Code sessions, the agents generated roughly 100,000 lines of code and reached the point where the compiler could build Linux 6.9 for x86, ARM, and RISC-V. That's impressive. Bun has also undergone a large migration from Zig to Rust, with its current repository instructions describing the project as primarily written in Rust with just one engineer. Microsoft is applying agents to codebase-wide refactoring, framework migrations, tests, modernization, and work across large repositories. Its own developer material explicitly presents agent mode as capable of changing multiple files and migrating legacy code to modern frameworks. Microsoft engineers are also working in increasingly common hybrid C++ and Rust codebases.
These examples do not prove that agents can autonomously replace an engineering organization. They prove something less dramatic and more useful: agents can already perform substantial work on real codebases when experienced people provide direction, constraints, and verification. Programming was moving toward higher-level composition long before “vibe coding” became a phrase. Engineers have spent decades installing, configuring, and combining software written by other people without reading every line of its source. The relevant question is whether you understand the abstractions you depend on, the decisions you are making, and the failure modes you are accepting. The same standard should apply to agents.
The difference between automation and vibe coding
Agents do not automatically produce vibe-coded software. People produce vibe-coded software when they accept systems they do not understand. You can create a mess by copying code from Stack Overflow, following a tutorial, installing the wrong framework, hiring an incompetent consultancy, or giving an AI agent unrestricted access to a repository. The agent is not the defining feature. The absence of engineering judgment is.
Real vibe coding happens when someone cannot explain:
- Why the architecture has its current shape.
- Why a dependency was introduced.
- Where business rules live.
- How transactions are handled.
- Which components are allowed to communicate.
- What happens when an external service fails.
- How data moves through the system.
- How to test, deploy, observe, or recover it.
- How to modify the software without asking the machine to rediscover everything.
That is a dangerous position, regardless of how the code was produced. Using agents responsibly means retaining ownership of the decisions while delegating parts of the execution. You still write code. Often you write quite a lot of it, because agents are imperfect and some tasks are easier to start or finish by hand. Sometimes I implement the core idea myself and use agents to propagate it through the system. Sometimes the agent builds the first version and I replace the parts I dislike. Sometimes it only investigates the repository and gives me an executable plan. The relationship is not “the agent codes and I watch.” It is closer to directing several extremely fast junior engineers who have read half the internet, never get tired, occasionally misunderstand obvious instructions, and remain capable of confidently constructing something absurd.
Start with rules, not prompts
The most important part of my setup is not a clever prompt. It is the collection of rules that defines how agents are allowed to work inside a project. I do not rely exclusively on a giant AGENTS.md file. Instead, I keep a small index and a structured .agents/ directory containing focused rules.
A simplified version looks like this:
AGENTS.md
.agents/
rules/
00-agent-behavior.md
01-project-boundaries.md
02-git-and-branching.md
03-surfacing-gaps-and-incomplete-work.md
04-file-headers-and-comments.md
05-security.md
10-naming-files-and-folders.md
11-constants-and-enums.md
12-abstractions.md
14-code-naming.md
15-control-flow-and-nesting.md
16-conditional-logic.md
...
AGENTS.md acts as the index. It tells the agent which rules exist, what they cover, and when they should be loaded. It matters because a single enormous instruction file is not necessarily better. Agents have finite context, and wasting that context on irrelevant rules leaves less room for the repository, the task, and the actual implementation. An agent working on a domain object does not always need the complete PostgreSQL migration policy. An agent updating Docker configuration does not need every frontend naming rule. The system should load the relevant constraints for the work being performed.
Rules must be short enough to remain usable and specific enough to influence behavior. Writing a book for every rule defeats the purpose. The agent will spend its context reading your constitution and have no space left to govern the country.
You can see an example of a monorepo with several rules and skills I normally use.
Define the relationship first
Before defining language conventions or architecture, I define how I want agents to work with me (the 01-agent-behavior.md file defines this). This is the universal behavior layer.
It covers things such as:
- Read the relevant documentation and project rules before changing code. (Yeah, that obvious)
- Do not silently invent requirements.
- Surface missing information and incomplete work.
- Explain uncertainty instead of hiding it.
- Do not change unrelated files.
- Do not install dependencies without approval.
- Do not weaken tests to make a build pass.
- Do not suppress errors that should be fixed.
- Show an implementation plan before large changes.
- Communicate with me in Spanish, but write code and technical identifiers in English.
- Keep explanations direct and reasonably short.
- Stop at destructive or irreversible operations.
- Ask for approval before deleting or replacing significant components.
These instructions may sound obvious to you, but they are not obvious to an agent. Agents are optimized to complete tasks. Without boundaries, they may choose the shortest path to a green test suite, even when that path involves changing the test, bypassing the architecture, adding an unnecessary package, swallowing an error, or quietly narrowing the original requirement. A useful rule does not merely describe the desired outcome. It also defines unacceptable shortcuts.
Rules should contain your engineering judgment
You should not blindly copy another person's rules and assume your project is now safe. Rules encode judgment. A respected Go engineer may publish an excellent set of conventions, but those conventions reflect that engineer’s experience, preferences, architecture, and operational environment. More on this one, every LLM will execute rules differently, based on context window power, trainning, etc. Your set of rules may not perform equally if you are using Opus 4.8 than Gemini 2.5. Copying them can produce good-looking code while leaving you unable to explain why the system works that way. That is only a slightly more sophisticated version of vibe coding.
You can use other people’s rules as examples. You can adopt rules you understand and agree with. What you should not do is outsource the criteria by which your own system will be judged. Your .agents/ directory should contain your knowledge, your team’s decisions, and your project’s constraints. If you do not understand the platform, the architecture, the infrastructure, and the trade-offs behind those rules, the agent cannot rescue you. It can only make your confusion much more productive.
Universal system rules
After the behavioral rules, I define language-agnostic system rules. These cover subjects that should remain consistent regardless of whether the implementation uses Go, Rust, Java, Swift, Kotlin, TypeScript, or something else:
- File and folder naming.
- Code naming.
- Constants and enumerations.
- Abstraction boundaries.
- Control flow and nesting.
- Conditional logic.
- Identifier generation.
- Comments and file headers.
- Git and branching.
- Security boundaries.
- Dependency approval.
- Transaction behavior.
- Migration safety.
- How gaps and incomplete work must be reported.
- How executable plans should be written.
- What information may be sent to an external language model.
This creates a stable base before adding rules for individual applications and layers.
For a monorepo, I separate universal rules from application-specific ones. A project containing a Next.js frontend, iOS and Android applications, a Go service, and a Rust service cannot be governed effectively by one undifferentiated instruction file. The repository should make it clear which rules apply everywhere and which belong to a particular application, language, or architectural layer.
monorepo/
├── AGENTS.md # Root index — entry point for all agents
├── CLAUDE.md # Claude Code pointer to AGENTS.md
├── .agents/
│ └── rules/ # Universal rules (apply to every layer)
│ ├── 00-agent-behavior.md
│ ├── 01-project-boundaries.md
│ ├── 02-git-and-branching.md
│ ├── 03-surfacing-gaps-and-incomplete-work.md
│ ├── 04-file-headers-and-comments.md
│ ├── 05-security.md
│ ├── 10-naming-files-and-folders.md
│ ├── 11-constants-and-enums.md
│ ├── 12-abstractions.md
│ ├── 14-code-naming.md
│ ├── 15-control-flow-and-nesting.md
│ ├── 16-conditional-logic.md
│ ├── 18-identifiers-and-uuidv7.md
│ ├── 19-llm-data-boundary-and-code-egress.md
│ ├── 20-persistence-and-postgresql.md
│ ├── 21-query-services.md
│ ├── 22-transactions.md
│ ├── 23-migrations.md
│ ├── 24-security-and-compliance.md
│ ├── 25-executable-plans.md
│ └── 26-dependency-audit.md
├── .claude/
│ └── skills/ # Executable Claude Code skills
│ ├── definition-of-done/
│ ├── plan-authoring/
│ ├── pr-check-release/
│ ├── pr-merge-dev/
│ └── release-check/
├── apps/
│ ├── web/ # Next.js (Bun workspace)
│ │ └── .agents/rules/ # Web-specific rules
│ ├── mobile-ios/ # Swift / SwiftUI
│ │ └── .agents/rules/
│ └── mobile-android/ # Kotlin / Compose
│ └── .agents/rules/
├── services/
│ ├── api/ # Go modular monolith + MCP server
│ │ └── .agents/rules/
│ ├── identity/ # Rust auth service
│ │ └── .agents/rules/
│ └── ai/ # Python AI layer (MCP client)
│ └── .agents/rules/
├── docker/ # Local backing infrastructure
│ └── .agents/rules/
├── docs/ # Architecture, product, process docs
└── tools/ # Local developer tooling
This structure also makes rules easier to review. A change to the persistence policy does not require editing a massive document containing frontend conventions, security requirements, and Git instructions.
Define the approved technology stack
Once the universal rules exist, I define the exact stack agents are allowed to use. This is more important than it sounds. Agents want to complete the task, and in the absence of constraints they will happily:
- Install a library you never approved, preferably a version old enough to have its own archaeology department.
- Use a language feature that has been obsolete for years.
- Target the wrong runtime.
- Add a second package that solves a problem the first package already solved.
- Copy an implementation written for a different framework version.
- Mix patterns from incompatible generations of the same platform.
- Replace a perfectly good standard-library feature with another dependency because that solution appeared more often in the training data.
- Repeat an insane amount of code that can be abstracted.
I have reviewed systems where different modules were effectively targeting different Java versions even though Java 21 was the approved standard. I have also seen Go code written in an older style while the project was already using a newer release with simpler and better-supported alternatives. The opposite problem is even worse: without an explicit stack policy, an agent may assume the wrong language, runtime, framework, or dependency version, generate a large amount of code, and only discover at compile time that the APIs it selected are unavailable or incompatible with the actual project.
Then, while the agent is still struggling to finish the task, the cheerful message arrives:
I see you are using an older version than expected. I will now update my implementation.
Excellent. Another mountain of tokens spent undoing work that should never have started.
The opposite happens too. I once had an agent generate a Zod schema using the Zod 3 style while the project was already on Zod 4. The code still worked, but it ignored the newer, preferred API. Nothing was technically broken. It was simply outdated on arrival.
The stack rules should define, at a minimum:
- Approved language and runtime versions.
- Approved frameworks.
- Existing libraries that should be preferred.
- Dependencies that are forbidden or require explicit approval.
- Testing frameworks.
- Formatting and linting tools.
- Build and validation commands.
- Deployment targets and runtime environments.
- Compatibility requirements.
- Whether experimental or unstable APIs are allowed.
Do not make the agent infer these details from whichever files it happens to read first.
Architecture requires decisions, not creativity
The next rule set defines the system architecture. This is not where I want the agent to express itself. I want decisions.
Is the system a monolith, a modular monolith, a set of microservices, a serverless application, a library, a command-line tool, or something else?
Where do business rules live?
Which direction may dependencies point?
How do applications communicate?
Which layer owns persistence?
Which parts of the system are allowed to know about the framework?
These questions must be answered before implementation begins. Otherwise, the agent may gradually produce something that is not a monolith, not a service architecture, and not anything else you could explain without drawing arrows for forty minutes.
During due diligence, I have met teams that believed they had built microservices when they had actually built a distributed monolith. They gained the deployment complexity of microservices while preserving the coupling of a monolith. A magnificent combination.
Architecture is not an aesthetic preference. It affects infrastructure costs, deployment complexity, observability, scalability, failure modes, staffing, and the speed at which the product can change. Many teams choose architectures because large technology companies use them. They copy the visible shape without inheriting the scale, constraints, operational maturity, or budget that made the original decision reasonable.
An agent will happily accelerate that mistake.
My preference: Clean Architecture
For many backend projects, I use Robert C. Martin’s Clean Architecture. You do not have to. Choose an architecture that fits the system you are building. A library, a microfrontend, a mobile application, a compiler, and a REST API do not necessarily need the same shape. What matters is making the decision explicit.
For Clean Architecture projects, I define rules for:
- Dependency direction.
- Domain isolation.
- Use cases and application services.
- Interfaces and adapters.
- Infrastructure boundaries.
- Data mapping.
- Framework isolation.
- Error handling.
- Transaction ownership.
- Testing responsibilities.
- Allowed imports between layers.
These rules prevent small violations from accumulating. The agent should not import a framework into the domain layer. It should not place business decisions in a PostgreSQL trigger. It should not spread the same validation logic across the controller, database, and domain model. It should not make the application layer depend on an infrastructure implementation because that happened to be convenient. Humans introduce these problems too. The difference is that an agent can introduce them across fifty files before lunch. Consistency is one of the greatest benefits of agent-assisted work, but only when you define what consistency means.
I do not trust myself either
The rules are not only for agents. I am human, which means I am also capable of rushing, overlooking files, forgetting conventions, and convincing myself that a questionable exception is perfectly reasonable because I would like to finish for the day. I do not trust that little bastard either.
When I think a task is finished, I run the /definition-of-done skill. It reviews the work against the project rules and executes the relevant automated checks.
Think of it as everything you should verify before running git push and making your teammates deal with whatever you forgot.
Depending on the repository, it may:
- Run unit, integration, and architectural tests.
- Apply formatters.
- Run linters and static analysis.
- Check dependency boundaries.
- Search for empty or abandoned files.
- Detect unused dependencies.
- Inspect unresolved markers and temporary code.
- Check migration consistency.
- Verify that generated artifacts are current.
- Confirm that the working tree is clean.
- Check whether all expected files are tracked.
- Compare the result against the original plan.
- Report incomplete requirements.
Most of these checks are performed by the agent using tooling I wrote, which is included in the example repository. The agent also verifies that the implementation covers the approved plan rather than merely compiling.
For example, teams sometimes create files during implementation and accidentally leave them empty or abandoned. My checks detect those files, determine whether anything references them, and report why they may exist. Once the finding is reviewed, the agent can remove them.
Go catches unused local variables and imports at compile time, but it does not detect every abandoned declaration, exported symbol, dead path, or forgotten file. Other ecosystems may provide even less protection unless stricter compiler and linting rules are explicitly enabled. As a result, empty files, unused exports, abandoned modules, and obsolete code paths can remain in a repository indefinitely. Without regular maintenance, repositories become cemeteries of previous intentions.
The Definition of Done prevents “the code compiles” from becoming the definition of completion.
Database rules deserve their own section
Database rules deserve their own section because agents can cause spectacular damage in the persistence layer.
Yes, PostgreSQL can do almost everything. That does not mean every workload belongs in PostgreSQL. You still need to choose the right storage system for the problem and define how each database is allowed to participate in the architecture.
In Clean Architecture projects, and in many other architectural styles, I often see business rules gradually pushed into PostgreSQL because it is convenient. UUIDs are generated in one place, calculations happen in another, validation is duplicated in the application, and eventually nobody can explain which layer owns the truth.
Agents are particularly vulnerable to this temptation because database features often provide fast ways to satisfy a requirement. If a task says, “Ensure this value is always updated,” a trigger may look like a successful solution. But if the architecture says business behavior belongs in the domain or application layer, that trigger may be technically effective and architecturally wrong.
Persistence rules should define:
- Which databases are approved.
- Which layer owns database access.
- How repositories are structured.
- Where identifiers are generated.
- Where business calculations may occur.
- Whether triggers and stored procedures are allowed.
- How transactions begin and end.
- How migrations are written and reviewed.
- How destructive migrations are handled.
- How queries are organized.
- How pagination and locking work.
- How retries and idempotency are implemented.
- How database-specific types are kept out of the domain.
- Which operations require explicit human approval.
The goal is not to ban every advanced database feature. It is to ensure those features are used deliberately, not simply because the agent found them convenient.
The future is not typing faster
The debate around AI-assisted development is often framed as a contest between “real engineers” and people asking a chatbot to build a startup. That framing is convenient because it avoids the more uncomfortable question:
What happens when experienced engineers use agents well?
An engineer who understands architecture, infrastructure, security, databases, operations, and product requirements can operate across a much larger area than before. The agent does not replace that knowledge. It multiplies the amount of execution that knowledge can direct.
This will change how engineering teams are built. Companies will not keep paying people to perform repetitive work manually simply because manual work feels more authentic. They will pay for judgment, ownership, technical direction, and the ability to produce reliable systems using the best tools available.
Typing every line of code or every command by hand will not protect anyone’s career. Neither will blindly accepting whatever an agent produces. The valuable engineer will be the person who can define the system, constrain the automation, inspect the result, understand the failures, and take responsibility for what ships.
That is not vibe coding.
That is digging massive holes without shovels or bare hands.