<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Julio Quezada</title>
    <description>The latest articles on DEV Community by Julio Quezada (@alejandroq12).</description>
    <link>https://dev.to/alejandroq12</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1102953%2F60ce3de6-c721-4c38-a393-cea16cf69447.jpeg</url>
      <title>DEV Community: Julio Quezada</title>
      <link>https://dev.to/alejandroq12</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alejandroq12"/>
    <language>en</language>
    <item>
      <title>Constraining the Agent: What I Learned Wiring MCP Into a Legacy System</title>
      <dc:creator>Julio Quezada</dc:creator>
      <pubDate>Sun, 26 Jul 2026 20:05:06 +0000</pubDate>
      <link>https://dev.to/alejandroq12/constraining-the-agent-what-i-learned-wiring-mcp-into-a-legacy-system-3i0e</link>
      <guid>https://dev.to/alejandroq12/constraining-the-agent-what-i-learned-wiring-mcp-into-a-legacy-system-3i0e</guid>
      <description>&lt;p&gt;&lt;em&gt;Notes from January 2026, when giving an AI agent live database access still felt reckless. Written for engineers who are about to do this, and for the leads deciding whether they should.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The moment that made me stop
&lt;/h2&gt;

&lt;p&gt;I had just finished connecting a coding agent to two things at once: the filesystem of a legacy PHP application, and the MySQL database behind it. Both with write access. It took about twenty minutes.&lt;/p&gt;

&lt;p&gt;Then I ran a test prompt, watched the agent read a table definition, cross-reference it against a model file, and correctly identify a column that existed in the database but had never been declared in the code, a real bug, in a system nobody had documented in years, and I felt genuinely excited for about ninety seconds.&lt;/p&gt;

&lt;p&gt;Then I thought about what else it could do with those same permissions.&lt;/p&gt;

&lt;p&gt;Nothing had gone wrong, to be clear. The agent had only read what I pointed it at. But it had only read because that's what I happened to ask, not because anything in that setup would have stopped it from doing more.&lt;/p&gt;

&lt;p&gt;I had just handed a non-deterministic process the ability to modify source files and issue arbitrary SQL against a system that people depend on. The capability was trivial to set up. The judgment about whether it &lt;em&gt;should&lt;/em&gt; have that capability was entirely mine, and nothing in those twenty minutes had asked me for it.&lt;/p&gt;

&lt;p&gt;That gap between how easy the capability is and how much engineering the constraint requires is what this article is about.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why grounding changes everything in legacy work
&lt;/h2&gt;

&lt;p&gt;The bottleneck in modernizing an old system has never been typing speed. It's context. You cannot safely change what you don't understand, and understanding is expensive because the knowledge is distributed across a database schema, a codebase, and assumptions nobody wrote down.&lt;/p&gt;

&lt;p&gt;This is exactly where AI assistants used to fall apart. Ask a model to query a table it has never seen and it produces something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- What the model guesses from a million tutorials&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Confident, clean, and useless because the legacy schema actually looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- What exists once the agent can read the schema&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id_usr_pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;correo_e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fec_reg&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;tbl_usr_sys&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;estado_reg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both queries are syntactically perfect. Only one of them runs.&lt;/p&gt;

&lt;p&gt;MCP closes this gap. Instead of guessing the schema, the agent queries it. Instead of guessing the model class, it reads the file. Generated code stops being a plausible looking artifact and starts being grounded in what actually exists.&lt;/p&gt;

&lt;p&gt;That's a real improvement. It's also what makes the security question urgent rather than theoretical, because grounding requires access, and access is the whole risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architecture: four layers of constraint
&lt;/h2&gt;

&lt;p&gt;What I ended up building was less about capability and more about blast radius.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1: A disposable database
&lt;/h3&gt;

&lt;p&gt;Before anything else, I built a dummy database, same schema, synthetic data, no relationship to anything real. Every agent driven exploration, every "what happens if we add this column," ran against the copy.&lt;/p&gt;

&lt;p&gt;This is not a sophisticated idea. It's the same instinct that stops you from testing a migration against production. But it's worth stating plainly, because the tooling makes it easy to skip: the connection string is the only difference between a safe experiment and an incident, and nothing in the setup process prompts you to notice which one you're configuring.&lt;/p&gt;

&lt;p&gt;The disposable database is also what makes everything downstream cheaper. When the worst case is "restore a snapshot," you can afford to let the agent be wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2: Least privilege on the connection itself
&lt;/h3&gt;

&lt;p&gt;A database MCP server has no permissions of its own. It executes queries as whatever user you gave it. If that user can &lt;code&gt;DELETE&lt;/code&gt;, the agent can &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The correct default is a dedicated account with &lt;code&gt;SELECT&lt;/code&gt; only, used for every task that involves understanding the system rather than changing it which, in modernization work, is most of them. Write access should be a deliberate escalation for a specific task, not the standing configuration.&lt;/p&gt;

&lt;p&gt;I understood this later than I should have. The configuration I first implemented used a full privilege account, and it worked, so I didn't question it. Working is not the same as correct.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3: An unreachable undo button
&lt;/h3&gt;

&lt;p&gt;Agents are good at shell commands, and shell commands are how you lose work. I added a permissions config that blocks version control operations outright. The shape looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.claude/settings.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;illustrative&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"deny"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git commit:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git push:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git rebase:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(git reset:*)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Bash(rm -rf:*)"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reasoning: git is the undo button. If the agent can operate the undo button, there is no undo button. Every other guardrail assumes I can revert to a known good state, so the mechanism that provides known good states has to sit outside the agent's reach.&lt;/p&gt;

&lt;p&gt;In practice this cost nothing. I commit manually, which I was doing anyway, and it means every change passes through a moment where a human reads a diff and decides.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 4: Separation of duties across agents
&lt;/h3&gt;

&lt;p&gt;Rather than one agent doing everything, I defined several with narrow mandates, each a separate definition in the project's &lt;code&gt;.claude/agents/&lt;/code&gt; folder:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implementer&lt;/strong&gt;: writes the change. Its job is to make the thing work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security reviewer&lt;/strong&gt;: reads the diff with an adversarial brief. Does not care whether the feature works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test author&lt;/strong&gt;: writes tests against the &lt;em&gt;stated behavior&lt;/em&gt;, deliberately without seeing the implementation reasoning, so it tests the requirement rather than the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA&lt;/strong&gt;: checks whether the change satisfies what was asked, including what was implied.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Me&lt;/strong&gt;: final review, and the only one who can commit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the security reviewer, trimmed to its essentials:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# .claude/agents/security-reviewer.md — illustrative
---
&lt;/span&gt;name: security-reviewer
&lt;span class="gh"&gt;description: Reviews diffs for security issues only. Run after any implementation.
---
&lt;/span&gt;
You receive a diff and nothing else. You do not evaluate
functionality, style, or performance.

Look specifically for:
&lt;span class="p"&gt;-&lt;/span&gt; SQL built by string concatenation
&lt;span class="p"&gt;-&lt;/span&gt; Authorization checks that trust client supplied identifiers
&lt;span class="p"&gt;-&lt;/span&gt; Secrets or connection strings appearing in code
&lt;span class="p"&gt;-&lt;/span&gt; Input that reaches file paths, shell commands, or queries unvalidated

Output: findings with file, line, severity, and the attack you would
attempt. If you find nothing, state exactly what you checked.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason to split roles is not that five agents are smarter than one. It's that a single agent asked to implement &lt;em&gt;and&lt;/em&gt; critique its own work in the same context will tend toward agreement with itself. A fresh context with an adversarial brief produces meaningfully different output than "now check your work." Separation of duties is an old control; it translates almost directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The constitution file
&lt;/h2&gt;

&lt;p&gt;Alongside the agents, a &lt;code&gt;CLAUDE.md&lt;/code&gt; at the project root with persistent instructions that load into every session. This is where you encode everything the model's generic priors will get wrong about &lt;em&gt;your&lt;/em&gt; system.&lt;/p&gt;

&lt;p&gt;A skeleton of mine, placeholders only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# CLAUDE.md — skeleton&lt;/span&gt;

&lt;span class="gu"&gt;## Hard rules&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Only ever connect to the development database. Never any other host.
&lt;span class="p"&gt;-&lt;/span&gt; Never run git commands. A human commits.
&lt;span class="p"&gt;-&lt;/span&gt; Never modify /vendor or applied migrations.
&lt;span class="p"&gt;-&lt;/span&gt; Schema changes are written as migration files, never executed directly.

&lt;span class="gu"&gt;## Things you would guess wrong&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; Primary keys follow &lt;span class="sb"&gt;`id_&amp;lt;entity&amp;gt;_pk`&lt;/span&gt;. Timestamps are &lt;span class="sb"&gt;`fec_*`&lt;/span&gt; and are
  stored as strings, not DATETIME.
&lt;span class="p"&gt;-&lt;/span&gt; "Deleted" rows are &lt;span class="sb"&gt;`estado_reg = 'B'`&lt;/span&gt;. Nothing is ever physically deleted.
&lt;span class="p"&gt;-&lt;/span&gt; Routing was customized in &lt;span class="nt"&gt;&amp;lt;file&amp;gt;&lt;/span&gt;; the framework docs do not apply there.

&lt;span class="gu"&gt;## Domain vocabulary&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="nt"&gt;&amp;lt;One&lt;/span&gt; &lt;span class="na"&gt;line&lt;/span&gt; &lt;span class="na"&gt;for&lt;/span&gt; &lt;span class="na"&gt;each&lt;/span&gt; &lt;span class="na"&gt;of&lt;/span&gt; &lt;span class="na"&gt;the&lt;/span&gt; &lt;span class="na"&gt;five&lt;/span&gt; &lt;span class="na"&gt;nouns&lt;/span&gt; &lt;span class="na"&gt;that&lt;/span&gt; &lt;span class="na"&gt;actually&lt;/span&gt; &lt;span class="na"&gt;matter&lt;/span&gt; &lt;span class="na"&gt;in&lt;/span&gt; &lt;span class="na"&gt;this&lt;/span&gt; &lt;span class="na"&gt;business.&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="gu"&gt;## Definition of done&lt;/span&gt;
&lt;span class="p"&gt;-&lt;/span&gt; A failing test existed before the fix and passes after it.
&lt;span class="p"&gt;-&lt;/span&gt; The security reviewer ran on the final diff.
&lt;span class="p"&gt;-&lt;/span&gt; Output states: what changed, why, and what you did NOT change.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The failure mode this prevents is subtle. Without it, every session starts from generic assumptions about how a PHP application should be structured, and the agent will helpfully suggest changes that would be correct in a modern codebase and catastrophic in this one.&lt;/p&gt;

&lt;p&gt;Treat it as a living document: every time the agent makes the same wrong assumption twice, that's a missing line.&lt;/p&gt;




&lt;h2&gt;
  
  
  What actually improved, honestly
&lt;/h2&gt;

&lt;p&gt;I want to avoid the version of this article that ends in a productivity multiplier.&lt;/p&gt;

&lt;p&gt;What genuinely got better: schema aware code generation stopped being wrong by default. Cross-referencing the database against the codebase, finding drift, undeclared columns, dead tables went from a manual audit to something I could ask for. My own understanding of unfamiliar corners of the system got faster to build.&lt;/p&gt;

&lt;p&gt;What did not get better: anything requiring judgment about whether a change &lt;em&gt;should&lt;/em&gt; be made. The agent will implement a bad idea with excellent syntax. It has no opinion about whether the requirement makes sense, and in legacy work, interrogating the requirement is frequently the actual job.&lt;/p&gt;

&lt;p&gt;And one thing got worse before it got better: review load. Generated code arrives faster than you can meaningfully evaluate it, and the temptation to approve a diff that merely &lt;em&gt;looks&lt;/em&gt; reasonable is real. The guardrails above exist substantially to protect against my own review fatigue.&lt;/p&gt;

&lt;p&gt;I'd love to close this section with a dramatic story of the agent attempting something destructive and a guardrail catching it mid-flight. I don't have one, and I've come to think that's the point. The incident that doesn't happen is invisible. You build the envelope precisely so that you never find out what the unconstrained version would have done.&lt;/p&gt;




&lt;h2&gt;
  
  
  The part organizations should think harder about
&lt;/h2&gt;

&lt;p&gt;Here is what an MCP server actually is, described plainly: a long running local process, holding credentials to your data, executing instructions that originate from a language model, installed from a public package registry, usually by a developer acting alone.&lt;/p&gt;

&lt;p&gt;Every clause in that sentence is a security consideration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The credentials sit in plaintext.&lt;/strong&gt; In my setup they lived in a wrapper script on disk. Better than pasting them into a shared config; still just a file with a password in it. I learned how fragile this is the mundane way: my first wrapper silently corrupted the password because the shell interpreted its special characters. Nothing was breached, the connection simply failed, but a secret that can be mangled by string interpolation is a secret being handled at the wrong layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The supply chain is wide open.&lt;/strong&gt; The standard invocation fetches and executes a package from a public registry at launch, resolving to the latest version unless you pin it. That package receives your database credentials in its environment. A compromised maintainer account or a typosquatted name is all it takes. Pin every version; for anything touching real data, mirror through a private registry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There's no audit trail by default.&lt;/strong&gt; When something goes wrong, "which tool call did that" is a question you want to be able to answer. Out of the box, you usually can't.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instructions can arrive from untrusted places.&lt;/strong&gt; If an agent reads a file, a database record, or a ticket containing text crafted to look like instructions, that text is now in the context of a process holding your credentials. This is a working argument for read-only defaults independent of everything else.&lt;/p&gt;

&lt;p&gt;None of this means don't use MCP. It means the developer installs community package pattern is a &lt;em&gt;prototyping&lt;/em&gt; pattern, and teams are quietly running it in production because it works and nobody stopped to reclassify it.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you lead a team
&lt;/h2&gt;

&lt;p&gt;The individual workflow above is a pilot. Turning it into a process is a different exercise, and it's mostly about deciding things once instead of per developer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with comprehension, not generation.&lt;/strong&gt; A read-only MCP connection against a replica turns "understand this legacy module" from weeks into days. It's the lowest risk, highest value entry point, and it builds the team's judgment about the tool before anyone asks it to write anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the envelope a team artifact.&lt;/strong&gt; &lt;code&gt;CLAUDE.md&lt;/code&gt;, the agent definitions, and the deny-list belong in the repository, reviewed like code. One developer's careful setup doesn't scale; checked in constraints do. When the agent makes a wrong assumption, fixing the constitution file is a commit anyone can make, the team's accumulated judgment becomes infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define the escalation path.&lt;/strong&gt; Who may enable write access, for which task, recorded where. If the answer is "whoever needs it, informally," you don't have a policy, you have a default you haven't noticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Watch review load as your leading indicator.&lt;/strong&gt; If merged diffs per day double and review time doesn't, quality is degrading silently. Cap change size per task at what a human can genuinely evaluate. The cheapest control in this entire article is a diff budget.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build your own MCP servers for anything real.&lt;/strong&gt; An internal server can be scoped to exactly the queries the work requires, read-only unless explicitly not, authenticated against your existing identity infrastructure, and audited by default. It's a small service, not a research project, and it's the difference between a capability you can defend in a review and one you're hoping nobody asks about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hire and train for the envelope.&lt;/strong&gt; The scarce skill is no longer producing code. It's designing the constraints code gets produced inside, and being able to tell whether what came out is correct. That has interview implications and mentoring implications, and the teams that internalize it first will quietly outbuild the ones optimizing for generation speed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Before connecting an agent to anything that matters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] A disposable copy of the data exists and is the default target&lt;/li&gt;
&lt;li&gt;[ ] The database user is &lt;code&gt;SELECT&lt;/code&gt; only; write access is a separate, deliberate step&lt;/li&gt;
&lt;li&gt;[ ] Every MCP package version is pinned&lt;/li&gt;
&lt;li&gt;[ ] Secrets are not sitting in plaintext inside a repo path&lt;/li&gt;
&lt;li&gt;[ ] Git is your undo mechanism, and is outside the agent's reach&lt;/li&gt;
&lt;li&gt;[ ] A &lt;code&gt;CLAUDE.md&lt;/code&gt; exists with the rules you'd give a contractor on day one&lt;/li&gt;
&lt;li&gt;[ ] Review roles are separated; the critic never sees the implementer's reasoning&lt;/li&gt;
&lt;li&gt;[ ] "Done" requires a test that failed before the change existed&lt;/li&gt;
&lt;li&gt;[ ] Diff size is capped at what you can genuinely review&lt;/li&gt;
&lt;li&gt;[ ] You can answer "which tool call did that" after the fact&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setup is twenty minutes. The engineering is everything on this list, and the list is just the same controls you'd apply to a capable contractor with commit access and no context, which is a reasonable mental model for what you've actually hired.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written from experience modernizing an internal system with an agentic workflow. All identifiers, schemas, and configuration values shown are placeholders.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>mcp</category>
      <category>mysql</category>
      <category>php</category>
    </item>
    <item>
      <title># Why a legacy project might be the best thing for your career</title>
      <dc:creator>Julio Quezada</dc:creator>
      <pubDate>Sun, 08 Feb 2026 08:33:22 +0000</pubDate>
      <link>https://dev.to/alejandroq12/-why-a-legacy-project-might-be-the-best-thing-for-your-career-4d0e</link>
      <guid>https://dev.to/alejandroq12/-why-a-legacy-project-might-be-the-best-thing-for-your-career-4d0e</guid>
      <description>&lt;p&gt;Up until recently, I thought working on a legacy project would slow down my career. Especially nowadays, with people posting about AI and shiny new tech every single second, it felt like being left behind.&lt;/p&gt;

&lt;p&gt;But after living through it firsthand, I've learned huge lessons that completely changed my perspective. A legacy codebase isn't a dead end, it's a hidden accelerator if you know how to use it.&lt;/p&gt;

&lt;p&gt;Here's what working on a legacy project actually taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. It forces you to become a better communicator
&lt;/h2&gt;

&lt;p&gt;Legacy projects are full of ambiguity. Requirements are unclear, documentation is outdated or nonexistent, and tribal knowledge lives in people's heads.&lt;/p&gt;

&lt;p&gt;This is exactly where you learn to communicate like a professional. You learn to ask the right questions, to say "I don't understand this yet" without fear, to double check assumptions before writing a single line of code, and to make sure what you deliver actually matches what was needed.&lt;/p&gt;

&lt;p&gt;These are the soft skills that separate a junior from a mid-level developer, and legacy projects hand them to you on a silver platter.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Messy code sharpens your ability to read and debug
&lt;/h2&gt;

&lt;p&gt;When you're dropped into a codebase with inconsistent patterns, zero tests, and creative variable naming, your brain is forced to adapt. You start recognizing structures, syntax combinations, and logic flows that clean tutorial code would never expose you to.&lt;/p&gt;

&lt;p&gt;Think about reading a sentence like this: 'Th d_veloper f_xed the b_g in pr_duction.' You probably read that without hesitation. Your brain filled in the gaps automatically. The same thing happens with code, the more messy codebases you read, the faster your brain learns to fill in the gaps and recognize what the code is doing, even when it's poorly written. This is how you develop real developer intuition, and there's no shortcut to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. It builds resilience that stands out on your resume
&lt;/h2&gt;

&lt;p&gt;Hiring managers know the difference between a developer who has only worked on greenfield projects and one who has survived and thrived in a messy, real world codebase. Being able to say "I maintained and improved a legacy system" signals that you can handle pressure, ambiguity, and imperfect conditions, exactly what production environments look like.&lt;/p&gt;

&lt;p&gt;Every developer should go through this at least once. You never know when you'll need that resilience again.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. It teaches you how to truly collaborate
&lt;/h2&gt;

&lt;p&gt;There will be moments when the code makes absolutely no sense and the only way forward is asking a colleague. Maybe they wrote it, maybe they've been on the project longer, or maybe they just see it from a different angle.&lt;/p&gt;

&lt;p&gt;These moments are gold. They teach you to collaborate with humility, and they often become the foundation of long lasting professional relationships. The developer who asks good questions earns more respect than the one who pretends to know everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. You'll develop a deep understanding of how things actually work
&lt;/h2&gt;

&lt;p&gt;When there's no documentation and no clear architecture, you have no choice but to follow the trace. You step through the code, you debug, you read database schemas, you figure out why something works even when it probably shouldn't.&lt;/p&gt;

&lt;p&gt;This process builds a skill that's incredibly hard to develop in a clean, well documented codebase: the ability to reverse engineer systems and understand them from the inside out. That skill transfers to every project you'll ever work on.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. It's the perfect environment to sharpen your tooling skills
&lt;/h2&gt;

&lt;p&gt;Legacy projects give you an excuse to lean into your dev tools. Debuggers, profilers, log analyzers, database query tools, you'll use all of them more than you ever would in a greenfield project. You can even introduce AI assisted tools to help navigate the codebase faster. Every tool you master here becomes a permanent part of your arsenal.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Word of Caution
&lt;/h2&gt;

&lt;p&gt;Make sure this is a chapter of your career, not the whole book. Spending too long in a legacy only environment without exposure to modern practices can genuinely slow you down. Take advantage of the lessons, then move on with a stronger foundation.&lt;/p&gt;

&lt;p&gt;And whenever you're coding, legacy or not, remember to have fun, be professional, deliver tons of value, and never stop learning.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>career</category>
      <category>development</category>
    </item>
  </channel>
</rss>
