A Counterintuitive Finding
If you write code with AI, which language gets you the most done?
Intuition says Python. Simple syntax, plenty of training data, generate and run. But after a few months of building two Rust projects, https_proxy and trans_proxy, with Claude Code, my conclusion is the opposite:
Rust may be the best language for AI programming.
Not because AI never gets Rust wrong. It gets plenty wrong. It is because the Rust compiler throws the mistake back at you, precisely, within milliseconds, which makes for an extremely efficient feedback loop. Python code "looks right" once generated, but the bug may be hiding in some corner of the runtime. Rust code that compiles has already had a whole class of errors eliminated.
This post follows the actual development of those two projects and argues why Claude Code + Rust ends up being a better programming paradigm.
The Compiler as Partner
Types: Verifying AI Output
Start with how trans_proxy defines its upstream proxy protocols:
enum ProxyProtocol {
HttpConnect,
Socks5(ProxyAuth),
}
enum ProxyAuth {
None,
UsernamePassword { username: String, password: String },
}
When the AI generates the code that handles an upstream connection, it has to cover every variant of ProxyProtocol:
match &proxy.protocol {
ProxyProtocol::HttpConnect => { /* HTTP CONNECT tunnel */ }
ProxyProtocol::Socks5(auth) => { /* SOCKS5 handshake */ }
}
If the AI drops the Socks5 arm, or forgets UsernamePassword authentication inside Socks5, the compiler errors out immediately. The code never gets a chance to run. This is not a lint warning, not a best-practice suggestion, it is a hard compile failure.
In Python the same logic probably comes out as a chain of if/elif, and a missing branch only fires a KeyError at runtime under some specific condition. The AI will not tell you what it missed, because it does not know either.
Borrow Checker: Automatic Proof
The DNS module in trans_proxy leans heavily on state shared across tasks:
// DNS lookup table: IP → domain mapping
let dns_table: Arc<RwLock<HashMap<Ipv4Addr, String>>> = ...;
// Query coalescer: avoids duplicate requests
let coalescer: Arc<QueryCoalescer> = ...;
AI-generated async code frequently shares data across tokio::spawn boundaries. In Go or Python, a data race is an occasional runtime event that may only surface under load testing. In Rust:
- Forgot the
Arcwrapper? Compile failure. Ownership cannot move across threads - Forgot the
RwLock? Compile failure. You cannot borrow mutably across threads - Called
.awaitwhile holding a lock? Clippy warns about the deadlock risk outright
The borrow checker turns "concurrency bugs a human has to spot in review by experience" into "type errors the compiler detects automatically". For AI-generated code that means you do not have to trust the AI's reasoning about concurrency. The compiler verifies it for you.
Exhaustive Match: No Gaps
Stealth detection in https_proxy is a good example:
pub fn is_proxy_request(req: &Request<Incoming>) -> bool {
if req.method() == Method::CONNECT {
return true;
}
if req.version() == Version::HTTP_2 {
return false;
}
req.uri().authority().is_some()
}
The logic looks simple, but the chain of judgments behind it is precise: CONNECT is always a proxy request; HTTP/2 that is not CONNECT never is (because the :authority pseudo-header is always present); for HTTP/1.x, check whether the URI carries an authority.
When the AI writes branch logic like this, Rust's exhaustive match makes sure every Method and every Version variant is accounted for. If hyper adds an HTTP/3 Version variant in the future, every uncovered match becomes a compile error automatically, instead of silently falling into a wrong fallback branch.
What Each Side Covers
AI and the Rust compiler each have obvious strengths and weaknesses, and they happen to complement each other:
| AI is good at | AI is bad at | |
|---|---|---|
| Pattern recognition | It has seen millions of HTTP parser implementations and writes a new one with ease | Judging whether a specific implementation is correct at the boundaries |
| API recall | Exact recall of the signatures and usage of tokio, hyper, reqwest | Making sure the order and combination of API calls is right on every execution path |
| Boilerplate | Seconds to write serde serialization, clap argument definitions, error type conversions | Keeping the generated type definitions consistent across the whole project |
The Rust compiler covers every item in the right column:
- Boundaries:
Option<T>forces you to handle the empty case,Result<T, E>forces you to handle errors - Execution paths: exhaustive match covers every branch
- Global consistency: the type system makes sure that when you change an interface in one place, every caller has to adapt
Take the configuration system in https_proxy. The AI produced a complete config struct:
#[derive(Debug, Deserialize, Serialize, Clone)]
struct Config {
listen: String,
domain: String,
acme: AcmeConfig,
users: Vec<UserConfig>,
#[serde(default)]
stealth: StealthConfig,
#[serde(default)]
fast_open: bool,
}
Annotations like #[derive(Deserialize)] and #[serde(default)] are something the AI remembers more accurately than most people. But if the AI changed the type of users from Vec<UserConfig> to HashMap<String, String> somewhere, every piece of code that touches users would fail to compile at once. Nobody has to grep the whole project for uses of that field.
Leverage: Feedback in Seconds
The Magic of cargo check
The diagram below contrasts the two feedback loops:
In the traditional workflow on the left, the human is the bottleneck. Reading, understanding and assessing a chunk of code can take several minutes. In the Rust workflow on the right, cargo check finishes type checking in seconds, and Claude Code reads the compiler output directly and fixes things itself. The whole loop needs no human involvement.
A typical case: while adding SOCKS5 support to trans_proxy, the AI's first pass forgot to range-check username against u8 in the ProxyAuth::UsernamePassword arm (SOCKS5 RFC 1929 caps usernames at 255 bytes). cargo check passed, but a boundary test in cargo test caught it. Once the AI saw the test output, its second pass produced the right validation logic. From error to fix, the whole loop took under 30 seconds.
Contrast: Dynamic Language Lag
If you wrote the same projects in Python:
- Type error? You wait for it at runtime, or rely on mypy (coverage is usually below 100%)
- Concurrency bug? It shows up sporadically under load, hard to reproduce reliably
- API mismatch? A successful import says nothing about whether the call is right, you wait until execution reaches that line
Delayed feedback means the AI's mistakes can propagate into everything written afterwards, and the cost of fixing grows exponentially. The Rust compiler pulls as much verification as possible forward to compile time, so every AI iteration starts from an already verified baseline.
Case Studies: Two Projects
https_proxy: Ten Modules
The https_proxy code is organized into 10 modules:
src/
├── main.rs # entry point and CLI
├── config.rs # YAML config parsing
├── tls.rs # ACME certificates and TLS
├── stealth.rs # stealth detection
├── auth.rs # Basic Auth
├── proxy.rs # CONNECT tunnel and HTTP forwarding
├── net.rs # TCP connections and Fast Open
├── service.rs # hyper service layer
├── setup.rs # TUI setup wizard
└── lib.rs # module exports
This structure was not designed up front. The process went roughly like this:
- I state the need (What): "I want an HTTPS forward proxy with automatic ACME certificates, stealth camouflage, and multi-user authentication"
- AI writes the first version (How): module layout and core logic from that description
- Compiler feedback (Correctness): type mismatches, lifetime errors, unhandled Results. Fixed one by one
- I add constraints (Why): "stealth detection has to distinguish HTTP/1.1 from HTTP/2, because HTTP/2 has different
:authoritysemantics" - Iterate to convergence: AI edits, compile, test, feedback, AI edits
Through all of it, I wrote almost no concrete Rust code. My job was defining requirements, explaining domain knowledge, reviewing architectural decisions. The AI turned that intent into a type-safe implementation. The compiler made sure the implementation did not drift away from the contract the type system defined.
trans_proxy: Systems Programming
trans_proxy was harder, because it involves a lot of platform-specific systems programming:
- Querying the pf NAT table on macOS through the
DIOCNATLOOKioctl - Getting the original destination address on Linux through the
SO_ORIGINAL_DSTgetsockopt - Binary parsing of the DNS protocol (manual byte manipulation)
- The state machine for the three-step SOCKS5 handshake
The AI's API recall shows clearly in cases like these. The parameter layout of ioctl, the constant values of socket options, the offsets in a DNS message: the AI remembers these details more accurately than a person. And Rust's type system guarantees things like:
// DNS query coalescing: the broadcast channel's type signature
// makes sure sender and receiver agree on the data type
let (tx, _) = broadcast::channel::<Vec<u8>>(1);
The DNS wire-format parsers the AI wrote (parse_query_name, parse_a_records, extract_min_ttl) use a lot of byte indexing. Code like that is prone to off-by-one errors, but Rust's array bounds checks panic at runtime instead of silently reading out of bounds. Even when the compiler cannot catch it at compile time, there is no memory safety problem at runtime.
New Paradigm: A Three-Way Split
Traditional programming is a person at an editor translating logic in their head into code. Since AI coding assistants arrived, a lot of people use them as "AI writes the draft, human revises it". In practice the human is still the one doing correctness verification.
Claude Code + Rust opens up a different division of labor:
The human owns What and Why:
- "I want a transparent proxy that intercepts gateway traffic and forwards it through an upstream CONNECT proxy"
- "DNS has to support DoH, because plain UDP risks poisoning"
- "Stealth detection has to distinguish HTTP versions, because HTTP/2 semantics differ"
The AI owns How:
- Writing the tokio async server skeleton
- Implementing the SOCKS5 handshake state machine
- Writing the DNS message parser
- Handling conditional compilation for platform differences
The compiler owns Correctness:
- The type system verifies interface contracts
- The borrow checker proves concurrency safety
- Exhaustive match kills missing branches
- Lifetime checks prevent dangling references
What makes this three-way split efficient is that the feedback loop is automated. After the AI writes code, nobody has to check whether the types are right or the concurrency is safe. The compiler answers in seconds. The human only has to engage at a higher level of abstraction: are the requirements right? Is the architecture reasonable? Is anything missing from the domain logic?
Not Just Rust
This paradigm is not unique to Rust. Any language with a strong type system and strict compile-time checks can benefit:
- Haskell: a stronger type system, but a smaller ecosystem and less AI training data
- OCaml: excellent type inference, but a smaller community
- TypeScript (strict mode): a weaker type system than Rust, but practical for frontend work
- Swift: value types and optionals give similar safety guarantees
Rust fits particularly well because it strikes the best balance between how strict the type safety is and how rich the real ecosystem is. Libraries like tokio, hyper, serde and clap are first-rate in quality and documentation, and there is plenty of AI training data for them.
Limits, Honestly
This paradigm is not a cure-all:
- The learning curve is still there. You have to understand Rust's ownership model to review the AI's code effectively. If you do not know Rust at all, the compiler's error messages are gibberish to you too.
- The compiler does not check business logic. Do the three rules in
is_proxy_requestcorrectly cover HTTP semantics? That needs human domain knowledge. The compiler only guarantees the code is "type correct", not "logically correct". - Compile time is the price.
cargo checkis fast, but a fullcargo build --release(especially with LTO on) can take several minutes. That is the tax on type safety. - Not every project needs Rust. One-off scripts, data analysis, quick prototypes. Python is still the better choice there. This paradigm suits system-level projects that need long-term maintenance, performance, and heavy concurrency.
Closing
Back to the question at the top: if you write code with AI, which language gets you the most done?
If the goal is "produce code that looks runnable as fast as possible", Python wins.
If the goal is "produce correct code as fast as possible", Rust wins.
Because in Rust's world the AI is not working alone. The compiler stands beside it and misses no type error, no unhandled boundary, no unsafe concurrent access.
Building https_proxy and trans_proxy convinced me that Claude Code + Rust redefines the division of labor between humans, AI and tools. The human focuses on the most valuable judgments (defining What and Why), the AI takes on the heaviest labor (implementing How), and the compiler provides the most reliable guarantee (verifying Correctness).
That is how I write code with AI right now.
