DEV Community

loach
loach

Posted on

Implementing 'You Can't' for AI Coding Agents

AI coding agents are getting better at writing software. I use them in my own open-source projects, too.

But there's a distinction I keep coming back to:

Teaching an AI what it shouldn't do is not the same as enforcing what its code is allowed to do.

We ask agents not to break architectural boundaries, introduce side effects, or add unapproved dependencies. Those instructions are useful, but instructions are not guarantees.

Prompts aren't enforcement

Imagine an agent generates this code inside a Domain layer:

namespace MyApp.Domain;

public class OrderService
{
    private readonly Infrastructure.OrderRepository _repository;
}
Enter fullscreen mode Exit fullscreen mode

If the referenced type exists, the C# compiler will not normally know that the dependency violates your architecture.

The code may work, compile, and still be unacceptable.

A README or AGENTS.md communicates a rule. It does not independently enforce it.

Move the decision outside the agent

The workflow I want looks like this:

AI agent or human writes code
             |
             v
      Compiler + Analyzers
             |
      Policy satisfied?
         /       \
       Yes        No
        |          |
   Candidate     Diagnostic
   for merge       |
                   v
                Revise code
Enter fullscreen mode Exit fullscreen mode

The agent can propose a solution. An independent tool decides whether the proposal satisfies the project's checkable constraints.

For C# projects, Roslyn Analyzers let us report diagnostics during compilation. Configured as errors, those diagnostics can make a violating build fail.

This gives AI agents immediate, deterministic feedback rather than another natural-language reminder.

Three open-source projects exploring the approach

ArchitectureAnalyzer: Compile the architecture contract

ArchitectureAnalyzer uses a JSON architecture contract and Roslyn diagnostics to check rules such as layer dependencies, forbidden APIs, and dependency graph constraints.

An architecture document records intent. A machine-checkable contract helps detect drift between that intent and the codebase.

PureSharp: Enforce specific code-level contracts

PureSharp introduces functional-programming-oriented checks for C#.

For example:

[PureMethod]
public static int Add(int a, int b) => a + b;
Enter fullscreen mode Exit fullscreen mode

It detects selected side effects within its documented purity contract. It also checks conventions for immutable local variables and FluentIf termination.

var _value = Calculate();
// _value = 100; // Diagnosed reassignment
Enter fullscreen mode Exit fullscreen mode

This is not a whole-program mathematical proof of purity. It's enforcement of explicitly defined, mechanically checkable properties.

PolicySharp: Allow by policy, deny by default

PolicySharp takes an allowlist-oriented approach.

A denylist catches violations someone remembered to enumerate. A new API or dependency may not appear on that list.

A default-deny policy instead follows this model:

Explicitly allowed    -> ALLOW
Explicitly denied     -> DENY
Unknown or ambiguous  -> DENY
Enter fullscreen mode Exit fullscreen mode

For example, a policy can describe which namespaces a Domain scope is allowed to depend on:

{
  "version": 1,
  "mode": "default-deny",
  "scopes": [
    {
      "id": "domain",
      "match": { "namespace": "MyApp.Domain.**" },
      "allow": {
        "namespaces": [
          "System",
          "MyApp.Domain.**"
        ]
      }
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This illustrates the policy model; precise enforcement coverage depends on the implemented version.

What if the agent changes the rules?

There's an obvious bypass:

  1. The agent introduces a prohibited dependency.
  2. An analyzer reports an error.
  3. The agent edits the policy or project configuration.
  4. The build passes.

That's why an analyzer alone isn't enough.

The authority to modify application code should be separate from the authority to weaken the constraints. Policy files, project files, and analyzer references may need independent approvals and CI checks.

Static analysis also has limits. It cannot enforce properties it does not analyze, and a build gate only matters when the project actually runs it.

Freedom to propose is not permission to merge

I don't want to constrain every implementation choice an AI makes.

I want to distinguish freedom to propose a solution from permission to accept the resulting change.

Within the approved boundaries, the agent can explore different approaches. If a boundary needs to change, that should become an explicit design decision instead of a workaround silently introduced during implementation.

Conclusion

Prompts communicate intent.

Analyzers enforce checkable rules.

CI validates changes.

Approval processes protect the rules themselves.

Instead of expecting AI to remember everything it must never do, we can create development environments that reliably detect prohibited changes before accepting them.

That is the direction I'm exploring with ArchitectureAnalyzer, PureSharp, and PolicySharp.

The goal isn't less capable AI. It's more predictable, maintainable AI-assisted software development.

Top comments (0)