Zerostack – A Unix-inspired coding agent written in pure Rust

Ever found yourself yearning for a coding environment that’s as elegant and efficient as Unix? I’ve been there, navigating through clunky IDEs and cumbersome setups, wishing for something that felt like home. That’s when I stumbled upon Zerostack—a Unix-inspired coding agent crafted in pure Rust. Let me share my journey with it, and why I think it’s worth your attention.

Discovering Zerostack

I first heard about Zerostack in a developer forum, where a few folks were buzzing about its lightweight nature and how it’s changing the way we interact with code. Intrigued, I decided to give it a whirl. What if I told you it’s like having a Swiss Army knife for coding? The simplicity and efficiency immediately struck a chord with me.

I was drawn to Rust for its performance and safety features, but I was skeptical about how a coding agent could improve my workflow. I remember the moment I installed Zerostack and ran my first command. The terminal felt alive, responsive, and best of all, it didn’t feel heavy or bloated like some of the other tools I’d tried.

The Beauty of Simplicity

One of my favorite features is how Zerostack embraces the Unix philosophy: “Do one thing and do it well.” I like to think of it as the minimalist’s dream. For instance, it offers a simple command to run a quick test suite:

zerostack run tests

In my experience, this simplicity allowed me to focus more on coding and less on navigating through complex menus or interfaces. I remember the first time I ran my tests this way—just a one-liner—and I felt a rush of excitement. It was like a breath of fresh air, reminding me how coding should feel.

Embracing Rust’s Power

Rust has this fantastic way of making you rethink your approach to programming. As I started using Zerostack more, I noticed how the language’s safety features translate into a more robust coding environment. I once made a silly mistake by trying to dereference a null pointer while testing a function. Rust, being Rust, threw a fit and stopped me dead in my tracks. I can’t tell you how many hours I’ve saved by avoiding those kinds of errors. A little bit of pain can lead to a whole lot of gain!

For example, here’s a snippet of Rust code for a simple HTTP request using Zerostack:

fn fetch_data(url: &str) -> Result<String, Box<dyn std::error::Error>> {
    let response = reqwest::blocking::get(url)?.text()?;
    Ok(response)
}

When I first implemented this, I was amazed at how the type system guided me through error handling. It’s like having a safety net that keeps you from falling too hard.

Real-World Use Cases

Let’s talk about actual applications. I’ve been building a personal project—a small web app for tracking my fitness goals. Using Zerostack, I integrated it with a Rust backend and a React frontend. The synergy between Zerostack’s simplicity and Rust’s performance proved to be a game changer.

There were moments, though, where I thought I’d hit a wall. I remember wrestling with state management in my React app. My initial approach was all over the place. But then I decided to encapsulate the logic within Zerostack and treat it like a state machine. This not only improved performance but also reduced the complexity of my codebase. It’s amazing how a shift in perspective can lead to breakthroughs.

Lessons Learned the Hard Way

Now, no journey is without bumps. I had my fair share of frustrations while setting up Zerostack with my existing projects. The documentation was decent but could use more examples. I spent hours trying to figure out why my build kept failing, only to realize I’d missed a simple configuration step.

So, here’s my advice: always check the configuration first! It’s like finding the missing piece of a jigsaw puzzle. The moment I corrected that, everything fell into place.

Troubleshooting Tips

If you decide to dive into Zerostack, keep these tips in your back pocket:

  1. Read the Docs: While it can be sparse, the documentation does have nuggets of wisdom—don’t skip over them!

  2. Experiment: Don’t be afraid to play around. I often set up a test environment where I can break things without fear of ruining my main project.

  3. Leverage the Community: The Rust community is vibrant and supportive. Ask questions, share your experiences, and you might just find that someone’s already solved the issue you’re facing.

Final Thoughts

I’m genuinely excited about Zerostack and what it brings to the table. It’s not just a tool; it’s a mindset shift back to the roots of efficient coding. I see it as a bridge that connects the old Unix philosophy with modern programming languages like Rust.

If you’re on the fence about trying it out, I’d say just go for it. The combination of Rust’s performance and Zerostack’s simplicity could very well revitalize your development experience. Who knows? You might just have your own “aha moment” like I did.

As I continue my journey with Zerostack, I look forward to more discoveries and maybe even some stumbles. After all, that’s how we learn and grow as developers, right? Here’s to coding adventures ahead!

Connect with Me

If you enjoyed this article, let’s connect! I’d love to hear your thoughts and continue the conversation.

Practice LeetCode with Me

I also solve daily LeetCode problems and share solutions on my GitHub repository. My repository includes solutions for:

  • Blind 75 problems
  • NeetCode 150 problems
  • Striver’s 450 questions

Do you solve daily LeetCode problems? If you do, please contribute! If you’re stuck on a problem, feel free to check out my solutions. Let’s learn and grow together! 💪

Love Reading?

If you’re a fan of reading books, I’ve written a fantasy fiction series that you might enjoy:

📚 The Manas Saga: Mysteries of the Ancients – An epic trilogy blending Indian mythology with modern adventure, featuring immortal warriors, ancient secrets, and a quest that spans millennia.

The series follows Manas, a young man who discovers his extraordinary destiny tied to the Mahabharata, as he embarks on a journey to restore the sacred Saraswati River and confront dark forces threatening the world.

You can find it on Amazon Kindle, and it’s also available with Kindle Unlimited!

Thanks for reading! Feel free to reach out if you have any questions or want to discuss tech, books, or anything in between.

Why npm supply chain attacks keep happening and how to harden your installs

When npm install becomes a security event

Look, I love npm. I’ve been shipping JavaScript for years and the ecosystem is genuinely incredible. But every few months we get another headline: a popular package gets hijacked, a maintainer’s token leaks, a typosquatted package siphons environment variables for a week before anyone notices.

The frustrating part? The advice is always the same — “be careful what you install” — as if you’re supposed to audit 1,200 transitive dependencies before every deploy.

Let me walk through what actually causes these incidents and what you can do at the project level. None of this is bulletproof, but the gap between a default npm install and a reasonably hardened install is bigger than most people realize.

Why the npm threat model is so messy

A typical Node project lists maybe 30 dependencies in package.json. Your node_modules ends up with 1,500. Every one of those packages can:

  • Run arbitrary code at install time via preinstall, install, and postinstall scripts
  • Get hijacked if the maintainer’s account is phished or their token leaks
  • Be replaced with a malicious version when ownership transfers to a new maintainer
  • Be typosquatted (lodahs vs lodash) and copy-pasted into a Dockerfile at 2am

The kicker: most of this happens before your tests run, before your linter runs, before any review. The moment you run npm install, you’ve already executed whatever code the package author wanted to run.

Step 1: Stop running install scripts by default

This is the highest-leverage change you can make. Drop this in your project .npmrc:

# Disable preinstall/install/postinstall scripts globally for this project
ignore-scripts=true

Or pass it explicitly:

npm install --ignore-scripts

Yes, this breaks packages with legitimate native-build steps — node-gyp, sharp, better-sqlite3. The workaround is to enable scripts only for the packages you actually trust. There’s no built-in allowlist, but you can rebuild specific packages after install:

npm install --ignore-scripts
# Rebuild only the native deps you trust
npm rebuild sharp better-sqlite3

I started doing this last year on a fintech project and the friction is real — but it’s a one-time setup cost per project, and it shuts down the most common payload-delivery path in npm supply chain incidents.

Step 2: Use npm ci everywhere except local dev

npm install is allowed to update your lockfile. In CI, that’s a footgun. If a transitive dep silently shifts to a compromised patch version, npm install happily picks it up.

npm ci does the opposite: it installs strictly from package-lock.json and errors out if the lockfile is out of sync with package.json.

# In your Dockerfile, GitHub Actions, etc.
npm ci --ignore-scripts

See the official docs for the full behavior. Combine this with pinned deps and you’ve removed the silent version-drift attack surface.

Step 3: Audit the lockfile, not just package.json

Most code reviews focus on package.json because it’s small. But the lockfile diff tells the real story — a one-line package.json change can introduce 80 new transitive dependencies.

When reviewing a PR that touches deps, look for:

  • New top-level packages you don’t recognize
  • Packages with very recent first-publish dates
  • Packages with one maintainer and millions of downloads (single-point-of-failure targets)
  • Suspicious names (typos, hyphenation tricks like cross-env-shell vs cross-env)

I’ll be honest: nobody manually does this for every PR. That’s what npm audit and Dependabot are for — but those mostly catch known CVEs, not zero-day supply chain stuff. The human eyeball check on lockfile diffs is still valuable for any load-bearing dep.

Step 4: Verify package provenance

npm added provenance attestations via Sigstore back in 2023. When a package is published from a CI pipeline with provenance enabled, you can verify which repo and which workflow built it.

You can inspect provenance from the CLI:

npm view <package-name> --json
# Look for the "attestations" field in the dist block

Packages with provenance are cryptographically tied to a public source repo and a specific build, which makes the “I phished the maintainer and published from my laptop” attack much harder.

It’s not universal — most packages still don’t ship with provenance — but for your own publishes, enabling it is one flag:

npm publish --provenance --access public

Step 5: Pin, proxy, and contain

A few additional defenses worth setting up once and forgetting:

  • Pin exact versions in critical projects. Drop the ^ and ~ in package.json. You give up automatic patch updates — you also stop new patch releases from running in prod five minutes after publish.
  • Use a private registry or proxy. Verdaccio is the open-source standard. It lets you cache, mirror, and gate which versions reach your team.
  • Run installs in a sandboxed environment. A locked-down container with no network egress except to the registry is a good starting point. If a postinstall script tries to phone home, the connection fails.
  • Generate an SBOM. CycloneDX has a free npm plugin. It won’t stop an attack, but it makes the post-incident question “are we exposed to package X at version Y?” answerable in seconds instead of hours.

What none of this fixes

Let me be straight: there is no configuration that makes npm install safe in an absolute sense. The trust model is fundamentally “we run code from strangers.” Every defense above raises the cost of an attack — it doesn’t eliminate it.

The realistic goal is layered. Reduce the blast radius (no install scripts). Slow down bad updates (lockfile + pinning). Increase visibility (lockfile review, provenance, SBOMs). Contain damage if something gets through (sandboxed installs, no secrets in the build env).

If you only do one thing this week, set ignore-scripts=true in your project .npmrc and figure out which native packages legitimately need to be rebuilt. That single change cuts off the most common payload-delivery path in real-world incidents.

The “no way to prevent this” framing is funny because it’s half true — you can’t prevent compromised packages from being published. But you absolutely can prevent them from executing in your build environment. The defaults are bad. Your project doesn’t have to inherit them.

Ten MCP servers I shipped this year. I use three.

I shipped ten Model Context Protocol servers under one npm scope this year. Three I use multiple times a week. Seven sit in my config file as dead weight. This is the honest ranking. The pattern is uncomfortable for the MCP-everywhere hype cycle, so I want to write it down.

The three I actually use

streamparse-mcp parses partial / truncated JSON. The trigger is the same every time: Claude is streaming output that should be JSON, the stream cuts off, I need to recover what was emitted. Asking the MCP is faster than remembering the JSON.parse recovery dance.

mcpcheck lints MCP config files. Errors in MCP configs are silent. Nothing crashes. The tool just is not there. A two-second lint catches the typo before ten minutes of “why does Claude not see my server.”

agentvet-mcp validates tool-call args before execution. Lets Claude pilot a tool I have not fully trust-listed. The LLM-friendly hints make Claude self-correct better than a stack trace would.

The seven I built and rarely touch

agentcast-mcp, agentguard-mcp, agentfit-mcp, agentsnap-mcp, promptbudget-mcp, citecite-mcp, ragmetric-mcp. Each one was a good idea when I shipped it. Each one is useful in scripted workflows. None of them earn their place in interactive chat.

The pattern

Two things separate the top three from the bottom seven.

Top servers fix things that happen often and silently. Bad MCP configs do not throw exceptions. Truncated streams do not crash. Hallucinated tool args run silently with the wrong inputs. Catching them at the moment they happen converts silent failure into a clear error message. That is high-leverage.

Bottom servers reproduce things Claude can already do. Arithmetic on small inputs, paraphrasing, summarizing one paragraph. Claude is fine at these. An MCP server that wraps “do this thing Claude can already do” is mostly redundant in chat. It earns its keep only in scripts where you need determinism.

The middle is “useful in scripts, quiet in interactive use.” That is real, but they shine in CI, not in your daily chat.

What I would build differently

I would build fewer MCP servers and more CLI tools.

Half my MCP servers should have been CLIs that I pipe into Claude. The MCP server is the right shape when Claude needs to decide whether to call the tool mid-conversation. The CLI is the right shape when I decide and want to paste the output.

If you are choosing between a CLI and an MCP server for a new idea, default to the CLI. Convert to an MCP server later if you find yourself wanting Claude to call it mid-conversation. Going the other direction is the path I keep regretting.

The uncomfortable bit

The MCP ecosystem will tell you to build a server for every problem. I built ten and use three.

If yours look like the bottom seven, that is not your fault. The tools are not bad. They are wrong-shaped for interactive chat. Move them to CLIs and you will use them more.

If you build something that fixes a thing that happens often and silently, you will end up in the top tier. That is the test.

All ten are in the official MCP Registry under my npm scope. The top three are worth the install. The rest are situational; install them when you hit the problem they solve, not before.

Five problems every agent loop has. No framework needed.

Most agent failure modes are not interesting. They are boring. They are the same five problems in different costumes. After eighteen months running agent loops in production, I keep meeting these five and only these five.

I do not build agent frameworks. I build small libraries that fix one failure mode each. You install the one you need. The composition emerges from your code, not a framework’s architecture diagram.

Here they are in roughly the order you will hit them.

1. The JSON is not JSON

Your model returns Sure, here you go:njsonn{...,}n“. You parse it. You crash.

Fix: repair before validate. Strip the fence. Extract the largest balanced JSON object from surrounding prose. Remove trailing commas. Then validate against your schema. If validation fails, send the model back a precise hint, not a generic “invalid JSON, please try again.”

The hint is the trick. Smaller models self-correct beautifully on a structural complaint. They do not self-correct on a vague reprimand.

2. The tool args are wrong

The model picks the right tool. It calls it with units: "kelvin" against an enum of ["c", "f"]. You run the tool. Bad things happen.

Fix: validate every tool call against its schema before running it. Validation issues become the tool’s response. Feed them back. The model fixes the call on the next turn.

Always return all validation issues at once, not the first. The model fixes them in one retry, not five.

3. The agent wanders to the wrong network

The minute your agent can pick URLs, you handed it URL-picking power. A confused-deputy bug or a prompt injection sends a fetch to a domain you did not authorize. By the time you notice, an API key is in an attacker’s log.

Fix: declarative domain allowlist. List the four hosts your agent legitimately needs. Block everything else with an error message at the HTTP layer.

4. The context budget runs out

You stack five turns of chat history. You drop the system message accidentally during truncation. The agent forgets what it is doing. Or you drop the trailing user turn and the model answers a question you never asked.

Fix: anchored truncation. Preserve the leading system message and the trailing user turn. Drop from the middle.

Drop-oldest is the right default for chat. Drop-middle is better when you want both early grounding and recent context. Both keep the load-bearing pieces of the prompt.

5. Regressions sneak in

You tweak a system prompt. The agent now picks tools in a slightly different order. Sometimes that is fine. Sometimes it is a regression that breaks the deployed app and you only notice next Friday.

Fix: snapshot tests for agent traces. Record one run end-to-end. First test run writes the snapshot. Later runs diff and fail with a unified diff if anything changed. Refresh with an env var when the change is intentional.

The framework I did not write

A naive agent loop hitting all five of these:

`rust
let fitted = Fitter::new(8_000).fit(messages, Strategy::DropOldest);
let raw = call_model(&fitted).await?;
let action = caster.parse(&raw)?;
if action.kind == "tool" {
validator(&action.tool)?.validate(&action.args)
.map_err(|e| anyhow!(e.for_llm()))?;
run_tool(&action).await
} else {
Ok(action.text)
}
`

Ten lines. Five concerns. Each concern is a separate 200-line library that does one thing and ships independently.

The framework version of this is 2000 lines, locks you to one HTTP client, opinionated about which provider, and bundles all five concerns into a single API you cannot pry apart when one of them is wrong.

I have shipped both kinds. The small libraries win every time.

If you are building an agent and have not hit problems 1 or 2 yet, you will. Skip the framework. Pick the small library when the problem actually shows up. Compose.

That is the whole stack.

How I Solved N-Queens Using Bitmasking (Step-by-Step Guide)

🚀 Solving N-Queens Using Bitmasking (Step-by-Step Guide)

🧠 Problem Overview

The N-Queens problem:

Place n queens on an n × n chessboard such that no two queens attack each other.

⚡ Why Bitmasking?

Instead of using:

  • Sets ❌
  • Arrays ❌

We use:

  • Bits (0/1) ✅ → faster & efficient

🔥 Core Idea

We track:

  • cols → occupied columns
  • diag1 → main diagonals (↘)
  • diag2 → anti-diagonals (↙)

Each is stored as a binary number

🧩 Step-by-Step Explanation (VERY SIMPLE)

Let’s understand with n = 4

🔹 Step 1: Initial State

Row = 0
cols  = 0000
diag1 = 0000
diag2 = 0000

👉 All positions are free

🔹 Step 2: Find Available Positions

available = ~(cols | diag1 | diag2) & ((1 << n) - 1)

Result:

available = 1111

👉 All 4 columns are available

🔹 Step 3: Pick One Position

pos = available & -available

👉 Picks rightmost 1

pos = 0001  → column 0

🔹 Step 4: Place Queen

Board:

Q...
....
....
....

Update masks:

cols  = 0001
diag1 = 0010   (shift left)
diag2 = 0000   (shift right)

🔹 Step 5: Move to Next Row

Now row = 1

Find available:

available = ~(0001 | 0010 | 0000) = 1100

👉 Only column 2 and 3 are free

🔹 Step 6: Repeat Process

Pick:

pos = 0100  → column 2

Place queen:

Q...
..Q.
....
....

Update masks and continue…

🔹 Step 7: Dead End? Backtrack!

If no positions available:
👉 Go back (remove previous queen)
👉 Try next possibility

🔹 Step 8: When Row == n

All queens placed successfully 🎉

👉 Save board as a solution

💻 Final Bitmask Code

class Solution(object):
    def solveNQueens(self, n):
        result = []
        board = ["." * n for _ in range(n)]

        def backtrack(row, cols, diag1, diag2):
            if row == n:
                result.append(board[:])
                return

            available = ~(cols | diag1 | diag2) & ((1 << n) - 1)

            while available:
                pos = available & -available
                available = available & (available - 1)

                col = (pos.bit_length() - 1)

                board[row] = board[row][:col] + "Q" + board[row][col+1:]

                backtrack(
                    row + 1,
                    cols | pos,
                    (diag1 | pos) << 1,
                    (diag2 | pos) >> 1
                )

                board[row] = board[row][:col] + "." + board[row][col+1:]

        backtrack(0, 0, 0, 0)
        return result

⚡ Key Tricks Explained

✔ Get all safe positions

available = ~(cols | diag1 | diag2) & ((1 << n) - 1)

✔ Pick one position

pos = available & -available

✔ Remove used position

available = available & (available - 1)

⏱️ Complexity

Time: O(N!)
Space: O(N)

👉 But very fast in practice 🚀

🧠 Easy Memory Trick

👉 “Use bits to mark attacks, and pick positions using binary tricks”

💡 Interview Tip

Say this confidently:

“I optimized N-Queens using bitmasking to reduce constraint checking to constant time.”

🔥 That’s a standout answer

🚀 Final Thought

Bitmasking turns a normal backtracking solution into a high-performance algorithm.

Once you understand this, you unlock a new level of problem-solving 💡

🔖 Tags

Algorithms #Backtracking #Bitmasking #LeetCode #CodingInterview #DSA

Why my Reddit scraper went from 92% to 61% success rate in 30 days (and the one-line fix)

Why my Reddit scraper went from 92% to 61% success rate in 30 days (and how I fixed it in one config flag)

I publish a small Reddit scraper actor on the Apify Store. It was my most-used actor: ~$30/mo in revenue, 70+ unique users per month, 92% success rate.

A month later, the success rate had collapsed to 61%. The actor was throwing on 40% of runs. Users stopped coming back. Revenue dropped to almost zero.

Here’s what happened and the one-line fix that brought it back.

The symptom

Every failed run looked like this in the logs:

2026-05-12T05:59:31  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (2/6)...
2026-05-12T05:59:34  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (3/6)...
2026-05-12T05:59:37  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (4/6)...
2026-05-12T05:59:39  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (5/6)...
2026-05-12T05:59:42  HTTP 403 (proxy IP likely blocked). Rotating proxy and retrying (6/6)...
Failed r/SideProject: Reddit blocked request after 6 attempts (HTTP 403).

Every proxy IP I could rotate to was getting 403’d. All six retry attempts, every subreddit. The retries weren’t broken — Reddit was just blocking the entire proxy pool.

Why this happened

Apify’s residential proxy pool gets shared across all customers running scrapers. If a few popular Reddit scrapers run thousands of requests per hour, Reddit eventually fingerprints those IPs and blocks them at the edge.

I checked: my retry logic already rotated proxies on every retry, used a varied set of User-Agent strings, and respected rate-limit hints. None of it mattered. The proxies were the bottleneck and I couldn’t get to fresh IPs because every proxy in the pool was already burned.

What surprised me was how fast it happened. 30 days from 92% → 61%. No code change on my side. Just IP reputation decay against www.reddit.com.

The fix that worked

Change one hostname.

- const BASE = 'https://www.reddit.com';
+ const BASE = 'https://old.reddit.com';

old.reddit.com is the same JSON API. Same routes, same response shape. But its bot-detection thresholds are dramatically more permissive — apparently because most legit human traffic moved to the redesigned www.reddit.com years ago, so the old subdomain’s load is lower and the WAF rules are looser.

After deploying the change to my actor (v0.1.18), the very next test run looked like this:

2026-05-17T14:57:43.069  Starting Reddit scraper: 1 subreddits...
2026-05-17T14:57:43.545  Fetching r/test page 1 (0 so far)...
2026-05-17T14:57:48.351  Done r/test: extracted 5 posts (saw 100 raw children).
2026-05-17T14:57:48.353  Reddit scraper finished. Pushed 5 items. 0 target(s) failed.

Zero retries. Zero 403s. Five posts in 4.8 seconds.

What else I added

While I was in the file, I also added:

  • Browser-like header set: Sec-Ch-Ua, Sec-Ch-Ua-Mobile, Sec-Ch-Ua-Platform, Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site, Accept-Encoding, Referer. Reddit fingerprints on these for any unusual combinations — anything missing is a tell.
  • 300–900ms jitter before each request: prevents the pattern-matching that catches scrapers running at a perfectly steady rate.
  • Two more User-Agent strings (Firefox + Safari) on top of the three Chrome variants, so the UA-rotation doesn’t degenerate into the same Chrome string 90% of the time.
  • Automatic fallback to www.reddit.com: if old.reddit.com returns 403 six times in a row, the actor switches the host and retries. (For now this never triggers, but it’s there.)

The actor is back to 92%+ success rate and processing live runs as I write this.

Why I’m sharing this

Three reasons.

One: if you’re running a scraper against Reddit (or any high-volume target), check whether you’re using www when there’s an old or m subdomain alternative. The WAF rules are often very different.

Two: in the proxy reputation game, you can’t out-rotate a poisoned pool. The architectural fix (switch endpoint) beats the tactical fix (more retries) every time.

Three: I publish a public Apify scraper that pulls Reddit posts and comments. It’s running on the fixed version right now. If you find it useful, a 30-second review on the Apify Store helps a solo developer compete with bigger publishers. I read every one.

I also do $9/mo curated Reddit digests by email if you’d rather not manage scraper runs yourself — stripe link here.

Renzo, solo dev. Building supabase-security, Apify actors, and other things at the intersection of “I should automate this” and “let me ship it as a product.”

I deleted 4080 lines and rebuilt from scratch. Here’s why call graphs are wrong for AI-generated code.

FCG overview: 6 epics arranged by user journey order, connected by semantic flow arrows

AI writes 5000 lines in 5 minutes. I need 3 hours to review them.
The code works. The tests pass. But I no longer understand my own project.

I tried fixing this 3 times. Failed 3 times. Deleted 4080 lines along the way.
Here’s what finally worked.

My take: every code visualization tool you’ve ever tried is showing you the wrong layer. Call graphs, dependency maps, AST trees — all wrong. Here’s why.

Attempt 1: Call graphs

First instinct: visualize how functions call each other. Tools like Madge and dependency-cruiser do this beautifully.

The result was a hairball.

parseRequest() → validateInput() → fetchUser() → comparePassword() → ...

Each node was a function name. Each edge was a call. Beautiful. Useless.

The problem: I didn’t want to know which function calls which. I wanted to know what the project does for the user. A call graph shows the how, not the what.

Imagine asking “What does scrambled eggs with tomatoes taste like?” and getting “prepare() calls slice() then whisk()“. That’s a call graph.

Attempt 2: AST + import maps

OK, so call graphs are too granular. Let me zoom out — module-level dependencies, AST analysis, the whole tree.

Tools I tried: ts-morph, PyCG, language servers.

Result: a prettier hairball. Now my nodes were files instead of functions, but the same fundamental problem — structure, not semantics.

I could see “auth/login.ts imports utils/jwt.ts” but that’s not “the user logs in by submitting their password, the system verifies it, then signs a JWT”.

The first sentence is structure. The second is the actual story.

Attempt 3: UCG (Universal Code Graph)

Now I got architectural. I designed a 5-layer system:

Language adapter (TS, Python) → UCG IR (universal graph)
  → Framework plugins → AI semantic layer → Canvas

The idea: structure is truth, semantics is decoration. Build a precise call graph at the bottom, let AI add labels on top.

I built it. It compiled. It rendered.

It still showed the wrong thing.

The semantic labels AI generated were like putting lipstick on the call graph. “src/graph/” got labeled “Canvas Rendering” — accurate but uninformative. “f-login” got labeled “User Authentication” — same thing the path already said.

The real insight: The structure of code (what functions call what) and the structure of features (what the project does for the user) are two different graphs. They’re not aligned. You can’t get from one to the other by adding labels.

I deleted 4080 lines of UCG code in one commit.

What finally worked: FCG (Feature & Flow Graph)

The breakthrough was admitting AI doesn’t need help finding the structure. AI already knows what each feature does — it just wrote them. So I let AI describe features directly, in user terms:

FCG overview: 6 epics arranged by user journey order, connected by semantic flow arrows
Overview view — 6 epics laid out in user-journey order, semantic edges between them.

{
  "id": "f-login",
  "name": "User Login",
  "steps": [
    { "name": "Receive credentials", "role": "input" },
    { "name": "Find user", "role": "data-read" },
    { "name": "Verify password", "role": "auth" },
    { "name": "Issue token", "role": "compute" },
    { "name": "Return token", "role": "output" },
    { "name": "Return auth failed", "role": "error" }
  ],
  "flow": [
    { "from": "input", "to": "find", "kind": "next" },
    { "from": "find", "to": "verify", "kind": "next" },
    { "from": "verify", "to": "issue", "kind": "conditional", "condition": "password correct" },
    { "from": "verify", "to": "fail", "kind": "conditional", "condition": "password wrong" }
  ]
}

This is what I actually want to see when reviewing AI work. Not “function A calls function B”. But “step 1 happens, then step 2, except when X then step 3”.

It’s the story of the feature.

I called this format FCG (Feature & Flow Graph) and built a viewer for it. AI writes the JSON during normal workflow (every code change → AI updates features.json). I read the graph, not the code.

Three principles that emerged

After validating on two real projects (a 40-feature simulation engine and a hackathon submission), three rules crystallized:

  1. Semantic control belongs to AI / features.json — naming, ordering, grouping. The frontend never infers semantics.
  2. Visual & interaction belongs to the frontend — drag, zoom, theme, layout algorithms.
  3. When uncertain, let AI write it down explicitly — no frontend heuristics. Schema gets richer, the canvas stays a pure consumer.

These rules saved me from rebuilding it a fourth time.

Real example: the tool modeling itself

Here’s what convinced me FCG works:

I had AI generate features.json for CodeSee itself. The graph shows install → scan → viewer → layout → sync as a clean user journey, with 22 features grouped under 6 epics, all the cross-feature relationships rendered as semantic edges.

Features view: 22 features grouped under 6 epic containers
Features view — drill down into any epic, see its features grouped in containers.

A new contributor can now understand the project in 30 seconds by opening the live demo — no need to read the code.

Drill down further and you see the actual flow inside a single feature:

Steps view: directed flow with async, conditional, and error branches
Steps view — async (dashed animated), conditional (with condition labels), error branches all rendered with distinct visual language.

What I learned

If you’re working with AI-generated code and feeling that same loss of ownership:

  • Don’t try to make sense of the code structure. The code is fine, you just can’t keep up with the volume.
  • Don’t try to summarize code with AI labels. Wrong abstraction layer.
  • Have AI describe what the code does for the user, in user terms. Then you read the description.

The best part: AI is already doing this work mentally when it generates code. It just doesn’t usually write it down. CodeSee’s prompts make it write it down.

Try it

CodeSee is open source (MIT) and the viewer runs as a static site — no install needed:

→ Live demo (modeling itself, drop your own features.json to try)
→ GitHub repo

Works with Cursor, Claude Code, Kiro, Copilot, Codex, Gemini CLI, and any AI IDE that reads AGENTS.md or SKILL.md.

I’m an independent developer building this in my spare time. If you give it a try, I’d love feedback — open an issue or find me on LinuxDo.

What’s the worst code visualization tool you’ve used? What do you wish existed instead? Drop a comment — I’m collecting pain points to drive the roadmap.

Building this took three rebuilds and two real projects to validate. One stable schema came out of it. If it sounds useful: a star helps me know I’m onto something — ⭐ here.

The Missing Engineering Stack for Production AI Agents

The “build an agent in 5 minutes” tutorials get you to a demo. They don’t get you to production. Here’s the field guide for the four primitives that decide whether your agent survives contact with real users, real data, and real adversaries — context-window discipline, skill composition, capability-based security, and drift telemetry. Concrete patterns, named tradeoffs, and the enterprise integrations that let you ship past prototype.

This is part 1 of a 3-post series. Part 2 — Why current IDEs need to be redesigned for the agent era — covers the developer-tooling argument. Part 3 introduces what I’m shipping next.

1. Tokens — context-window discipline
A token is the unit of inference cost, the unit of latency, and the unit of model attention. Treat it like memory in a 1990s embedded system: budget every byte, evict aggressively, and never assume the next call gets the same allocation.

Prompt caching is a 90% cost cut you’d be insane to ignore
Anthropic’s cache_control: { type: ‘ephemeral’ } marker (5-minute TTL by default, 1-hour via the extended-TTL beta) deduplicates the static prefix of your prompts at the inference layer. Cached tokens are billed at 10% of input cost; cache writes cost 25% more on the first call. The math: any system prompt + tool catalog + few-shot exemplar bank that’s reused more than ~3 times per 5 minutes is a net cost win. Order matters — the cache is a prefix, not a content-addressable store, so the cached span has to be byte-identical and at the start.

messages: [
{ role: “user”, content: [
{ type: “text”, text: STATIC_TOOL_CATALOG, cache_control: { type: “ephemeral” } },
{ type: “text”, text: STATIC_SKILLS_BUNDLE, cache_control: { type: “ephemeral” } },
{ type: “text”, text: dynamicUserTurn },
]}
]
Two cache breakpoints because cache reads accumulate up to the most recent cache_control marker — splitting tool catalog from skill bundle lets either evolve without busting the other. OpenAI’s automatic prefix caching (no opt-in, but no extended TTL) and Gemini’s explicit CachedContent resources are the equivalents on the other major providers.

Model routing — pay Haiku rates for Opus-class outcomes
A single agent run rarely needs the same model for every step. The cost spread is enormous: Claude Haiku 4.5 is $1/$5 per million in/out, Sonnet 4.6 is $3/$15, Opus 4.7 is $15/$75. The pattern that’s worked for me is a three-tier router:

  • Retrieval / classification / extraction → Haiku. Use structured outputs (forced JSON via tool_use with strict mode) so the model can’t waste tokens on freeform.
  • Synthesis / reasoning over retrieved context → Sonnet. The default mid-tier; this is where 80% of business logic lives.
  • Tool selection / planning / disambiguation → Opus only when the planner has to coordinate >5 tool calls or weigh ambiguous user intent.

Switching costs ~50ms of router latency. The cost amortization is typically 4–8× on production workloads. The trap: don’t route based on input length alone — route based on the step type. A 50-token “is this a refund request?” classifier on Haiku is 60× cheaper than the same call on Opus.

Streaming, KV reuse, and the structured-output dodge
Streaming via SSE (Anthropic, OpenAI) or gRPC bidirectional (Vertex) is non-negotiable for latency. The first token typically lands at 200–600 ms; the full response at 2–8 seconds. If your UX waits for the full response, you’ve added 4 seconds of perceived latency for zero product reason.

KV cache reuse across calls is the under-discussed companion to prompt caching. Modern Anthropic and OpenAI back-ends keep the attention key-value cache warm across the cache TTL. Order tool calls so the most-frequently-called tools come first in your tool list, because tool definitions are part of the prefix that gets cached.

The structured-output dodge: when you need a list, a classification, or a structured fact, don’t ask the model in freeform — define a tool, force it via tool_choice, and receive a typed JSON object. You skip 50–80% of the freeform tokens the model would otherwise generate, and the output is parser-safe by construction. Pair with strict mode (OpenAI) or JSON Schema with $defs (Anthropic) to refuse off-schema outputs at the decoder.

2. Skills — composition, not prompts
A “skill” is the unit of behavior an agent can perform. Most production agents conflate three different things into a megaprompt: identity (who are you), capabilities (what can you do), and policies (what you must / must not do). That conflation makes prompts impossible to evolve safely. Separate them into composable fragments, then assemble at runtime.

The model I’ve shipped against — and what I think every production agent eventually converges on — is the trigger / action / restriction triple per skill:

{
“id”: “refund-policy-2024”,
“trigger”: “the user asks for a refund”,
“action”: “verify the order is within the 30-day window, then issue a refund via tools.stripe.refund and post-confirm via tools.email.send”,
“restriction”: “never issue refunds > $500 without a human-approval gate; never refund subscription items in their first cycle”
}

Domain experts (PMs, ops, legal) author triples in plain English. The runtime composes them into a system-prompt slot. Versioning per skill — not per agent. Eval suites attach to the skill, so swapping out a refund policy in 2026 doesn’t require reblessing the entire agent.

Tool use, MCP, and the transport question
Tools are the IO of an agent. The schema is the contract. Two opinions worth holding:

  • Strict JSON schemas with additionalProperties: false. Closed-world schemas catch hallucinated arguments at the validator instead of in production. Strict mode (OpenAI) and the Anthropic tool_choice + JSON-Schema combo both enforce this.

  • Tools should be small and idempotent. orders.refund(orderId, amountCents), not orders.handle(intent, payload). The agent’s planner is dramatically more reliable when each tool does one thing with a typed input.

Once you have more than ~5 tools, the catalog itself becomes worth standardizing. Model Context Protocol (MCP) — Anthropic’s open-source agent ↔ tool spec — is the answer that’s consolidating the ecosystem. Three transports, three different tradeoffs:

  • stdio — local-process tools. Lowest latency, zero network surface. Use this for code execution, filesystem ops, anything sensitive.

  • SSE (deprecated in favor of StreamableHTTP) — long-poll over HTTP. Browser-friendly, easy to host. Latency ~50ms.

  • StreamableHTTP — single-endpoint HTTP with optional SSE for streaming responses. The current recommendation for hosted MCP servers. Compatible with most cloud LB stacks.

The plan-execute-review loop
For agents with >3 sequential tool calls, prompt the model to plan first (one message, no tool calls), execute against that plan (n messages, tool calls only), then review the result against the plan’s stated success criteria (one message, no tool calls). Anthropic’s Agent SDK ships this pattern via the plan_mode primitive; it’s also straightforward to implement in raw fetch with three system-prompt slots.

The bonus: when the agent fails, the failure is grounded in a textual plan you can replay, eval, and red-team — instead of an opaque chain of tool calls.

3. Security — capability-based, not vibe-based
The threat surface of an agent is wider than people pretend. A short list:

  • Prompt injection — adversarial input in retrieved context, tool outputs, or user data flips the agent’s instructions.

  • Data exfiltration — the agent calls a tool that emits sensitive data to an attacker-controlled destination (an email, a webhook, a markdown image with a query string).

  • Tool abuse / RCE — the agent uses a legitimate tool in a way the designer didn’t intend (a shell tool, a code-exec tool).

  • Supply chain — a tool dependency or model weight is compromised.

  • Secret leakage — API keys end up in logs, prompts, or tool error messages.

Capability-based authority, not ambient authority
The security primitive that’s stood up best in 50 years of OS research is the object capability: hand a process the smallest unforgeable token that lets it do exactly the thing it needs, and nothing else. Apply this to agents.

Concretely: don’t give the agent a long-lived OPENAI_API_KEY with billing access. Give it a per-session token, scoped to specific endpoints, with a TTL. Every tool gets a separate principal. Authorize via OAuth 2.1 with PKCE — the agent walks the user through delegated authorization, the user sees the exact scopes, and tokens are stored in the OS keychain (libsecret on Linux, Keychain on macOS, DPAPI on Windows; Electron’s safeStorage wraps the platform primitive for cross-OS).

Sandbox the tools, not just the agent
If a tool runs untrusted code or writes to a filesystem, isolate it. Three real options ranked by overhead:

  • WASM (Wasmtime, Wasmer) — sub-millisecond startup, deny-by-default I/O, easy to configure capability lists. The right choice for code-exec and policy-evaluation tools.
  • gVisor — userspace kernel; near-full Linux compatibility with a 10–100ms startup cost. Right for tool subprocesses that need the full POSIX surface.
  • Firecracker — microVM; ~125ms startup, hardware-backed isolation. Right for multi-tenant agent execution in shared infra.

ko/distroless container images, SLSA Level 3 build attestation, and sigstore-signed artifacts close the supply-chain surface. If your agent runs in a long-lived process, write the SBOM to the artifact registry and gate deploys on cosign verification.

Prompt injection defense
The most under-addressed threat. The mitigations that actually work:

  • Channel separation. Treat tool outputs and retrieved documents as data, not as instructions. Anthropic’s recent research on instruction-data separation in the system prompt is the current best practice — wrap untrusted content in clearly labeled XML-ish tags and tell the model to ignore any instructions inside them.

  • Allowlist tool surfaces. The agent can call send_email only to addresses on a per-conversation allowlist that the user explicitly authorized. The same pattern applies to outbound HTTP, database writes, file outputs.

  • Output content classifiers. Run a small model over the agent’s tool calls before they execute, looking for known exfil patterns (suspicious destinations, base64-encoded blobs, sensitive-field references).
    HITL gates on consequential actions. Anything that costs money, sends external communication, modifies a database, or touches PII goes through a human approval before execution. The threshold is per-skill.

4. Trust — telemetry, not vibes
“It worked when I tested it” is not a trust story. The four signals you actually need on every agent in production:

Eval pass rate against a golden set
A regression suite of input/output pairs the agent must continue to pass. Run on every prompt change, every model upgrade, every tool catalog edit. Tag failures by skill so you can localize regressions. Pairwise LMSYS-style judging works for tone-sensitive outputs; exact-match works for structured outputs. Don’t conflate them.

Drift detection
Even with a stable model, your agent’s behavior drifts when the input distribution shifts — new product launches, seasonal traffic, adversarial probing. Track distribution shift on input embeddings (cosine distance from a reference centroid) and behavioral metrics (tool-call mix, refund rate, escalation rate). Alarm at 2σ; investigate at 1σ.

Behavioral canaries
Plant N synthetic inputs per day designed to exercise the prompt-injection, exfil, and jailbreak surfaces. Pass rate on canaries is your live red-team signal. When a new attack class appears in the wild, add it to the canary set; you’ll know the next time someone tries it.

Audit trail with integrity
Every run captured as JSONL — input, system prompt, tool calls, model responses, costs, latencies. Hash chain over the events; periodically anchor the head into an immutable store (S3 Object Lock, GCS Bucket Lock). When auditors ask “what did the agent do on March 12 at 14:22 UTC”, you have a Merkle-verifiable answer.

A composite TrustScore rolls these up: weighted blend of eval pass rate, drift score, canary survival, HITL approval rate. Per agent, per skill, per day. The score is operationally meaningful only if it’s grounded in those underlying signals — a score with no traceable inputs is theater.

The compliance + enterprise integrations
For anything regulated — health, finance, government, EU operations — the trust telemetry has to map onto external frameworks. The integrations I’ve found genuinely useful:

  • TrustModel.ai for the GRC overlay — NIST AI RMF, ISO 42001, EU AI Act Article-by-Article mapping, SOC 2, FedRAMP. The TrustScore feeds directly into the control library and produces auditor-ready reports without re-instrumenting the agent.

  • Cisco DefenseClaw — Apache 2.0, free, OSS. Jeetu Patel announced it from the RSAC 2026 keynote stage on March 23, 2026; it’s the most consequential agent-security release of the year. Four components ship in the box: Skills Scanner (capability scan before execution), MCP Scanner (allow/block on MCP server inspection), CodeGuard (static analysis for secrets, unsafe deserialization, weak crypto, and injection patterns), and a Guardrail Proxy (runtime inspection of prompts, completions, and tool calls via regex rules + optional LLM judgment). Stack is a Go gateway sidecar + Python CLI + a TypeScript plugin for the OpenClaw framework that DefenseClaw was built to protect. The framework is observable by default, with first-class Splunk connectivity for the audit-trail story above. It bridges the trust gap that has 85% of enterprises experimenting with agents but only 5% running them in production. Personal note: Jeetu Patel is one of my role models, and I started coding the integration into the IDE I’m shipping the moment he walked off the RSAC stage. The most quoted line from the announcement — “I run OpenClaw at home — that’s exactly why we built DefenseClaw” — is the right framing. There’s no good reason not to wrap DefenseClaw around every production agent.

  • OpenTelemetry GenAI — the emerging standard for agent telemetry semconv. Emit the standard span attributes (gen_ai.system, gen_ai.request.model, gen_ai.usage.input_tokens) and your traces work in any OTel-compatible backend.

The bar
A production agent is not a model and a prompt. It’s a token economy, a skill catalog with versioning, a capability-scoped security model, and a trust telemetry stack. Each of those is a non-trivial engineering surface in its own right; together, they’re more work than the “build an agent in 5 minutes” tutorials acknowledge.

The argument I’ll make in part 2 is that the IDEs we have weren’t built to help engineers hit this bar. They were built for the 2010 unit of work — one developer, one project, one file at a time — and the unit of work in 2026 is an agent that gets trained, guard-railed, and overseen by a domain expert who isn’t the engineer. The tooling has to follow.

Music and Code Ask the Same Thing of Me

I have had this feeling for a long time: music, and especially composition, is much closer to software development than we usually admit.

The obvious explanation is mathematics. That part is true, of course, but it is also the easiest place to stop: music has numbers behind it, code has numbers behind it, so the two must be related.

I think that is true, but I also think it is the least interesting version of the truth.

The deeper connection, for me, is not mathematics. It is the feeling of parts trying to find their place.

When I write music, I rarely feel like I am simply inventing a song from nothing. It feels more like different pieces slowly reveal what they want to be in relation to each other. A motif begins as a small gesture. A chord progression may feel important for a while, then turn out to be only a passage. A melody can work on its own and still not belong in the center. The song keeps asking where each thing should live.

I feel almost the same thing when I write code.

A module begins as a piece of behavior. A function does not yet know where it belongs. An API has not found its natural shape. Sometimes something works technically, but it has not found its role. It is inside the system, but it does not sit there well. Like an instrument playing the right notes in the wrong place.

One of the most beautiful moments in composition is when a part finally lands where it belongs. Not because it became louder, or more complex, or more impressive. Because suddenly it becomes clear why it is there. The song does not merely contain it. The song needs it. The part is not decoration, not a clever idea, not a little display of skill. It has a role.

The same thing happens in good software.

What impresses me in a beautiful system is not that it contains many things. It is that everything has a reason to be there, and the parts are not fighting each other. Names clarify instead of merely labeling. Boundaries feel calm. The system starts to feel proportionate from the inside.

That sense of proportion is why beautiful code feels musical to me.

I do not mean that only as a metaphor. There really is such a thing as rhythm in a piece of code. There is breath. There is tension and release. Sometimes a refactor feels exactly like removing an unnecessary voice from a song: the result is not poorer, but clearer. Sometimes an abstraction behaves like a good variation on a theme. It does not hide the original idea. It reveals that the same form has been working in more than one place.

And there is code that works, but feels musically false.

It is not necessarily buggy, or slow, or even badly written according to any objective checklist. But something is off. The emphasis is in the wrong place. One part asks for too much attention while another has no room to breathe. The system is full of decisions that can each be justified in isolation, but together they do not sound right.

This is hard to explain to someone who sees programming only as the production of solutions. In the same way, it is hard to explain why a piece of music is not good merely because every note is “correct.” Correctness is the price of entry. The real work begins when something already functions, but is not yet true. When the song can be played, but does not yet say what it is supposed to say. When the code runs, but has not yet found the form in which I can leave it alone.

I think composition and software development demand the same kind of care from me.

They ask for a kind of attention that is not satisfied by the fact that something exists. I keep asking whether it exists in the right place, whether it is speaking in the right voice, whether it leaves enough room around itself. And underneath all of that, I am asking whether the whole thing is actually moving somewhere.

This is why music and code do not feel like separate worlds to me. They feel like two different materials for the same inner work. One uses sound. The other uses running systems. One unfolds through time. The other unfolds through use. But in both, I have to listen for structure, remove what does not belong, and wait for the moment when the form begins to look back at me.

Maybe this is why bad code can hurt so much.

Not only because it is hard to work with. Because I can hear the missed music inside it. I can hear that it could be cleaner, quieter, more generous to the next person who has to enter it and keep thinking.

And maybe this is why a good system can be so joyful.

Because it is not only useful. It is not only clever. There is a beauty in it that does not come from decoration, but from the fact that its parts finally understand why they are together.

I think this is why I keep returning to both. In music and in software, I am looking for the same moment: when many separate pieces stop being separate. When the parts begin to serve the same thought. When I no longer have to force the thing to hold together, because the structure has started to carry its own weight.

I wonder how many other people feel this.

How many developers are also musicians, somewhere? Maybe only after work, inside unfinished demos and songs that were never released. And how many of them found a home in software for exactly this reason: because the same compositional pleasure was waiting there, made from different material?

The more I think about it, the less this feels like a coincidence.

Something in them seems to come from the same place.

If you feel it too, I would love to know where music and code meet for you.

The Delusion of Infinite Compute: Running Gemma 4 on an i5 CPU

Diagram showing Gemma  distilling knowledge into Gemma 4

This is a submission for the Gemma 4 Challenge: Write About Gemma 4

TL;DR: You don’t need an RTX 5090 or a cloud budget. This guide shows you how to run Google’s Gemma 4 on a stock i5 CPU with 16GB RAM — using Rust, AVX2, quantization, TurboQuant KV compression, and thread pinning.

What Gemma 4 Actually Is

Before we talk about running it, you need to understand what you’re actually running — because Gemma 4 is not one model.

Google’s official model overview describes it as a family of three distinct architectures, each designed for a different hardware reality:

Diagram showing Gemma  distilling knowledge into Gemma 4's four model variants along with their use cases

Small (E2B and E4B) — Built for phones, edge devices, and browser deployment. Native multimodal input. 128K context window. This is the model that changes everything for constrained environments. Community benchmarks indicate the E2B can outperform the previous generation’s 27B model on specific reasoning tasks, despite being a fraction of the size.

Dense (31B) — A server-grade model that bridges local execution and cloud performance. The one you reach for when you want maximum capability on a single machine. It scores 85.2% on MMLU Pro and 89.2% on AIME 2026 math benchmarks.

Mixture-of-Experts (26B MoE) — Highly efficient, built for high-throughput reasoning. It carries 26 billion total parameters but only activates about 3.8 billion per token. The result: you get 27B-class reasoning at roughly the compute cost of a 4B model.

The existence of the E2B model is the most important thing about Gemma 4. Not because it’s the most powerful. Because it’s the most accessible.

Why Gemma Specifically?

Why optimize for Gemma instead of other open-source alternatives? Three reasons stand out for constrained hardware:

High density. Gemma punches above its weight class. The 26B MoE, for instance, scores 79.2% on GPQA Diamond — ahead of OpenAI’s gpt-oss-120B at 76.2%. That’s a 94-billion-parameter gap. Fewer parameters, better results.

Compression-friendly architecture. Gemma 4 is designed to hold up under aggressive quantization and KV caching schemes. Google built it using knowledge distillation from Gemini, training smaller models to mimic the reasoning patterns of much larger ones — which is a key reason the models hold quality even after being compressed.

Open license. Gemma 4 ships under Apache 2.0. You own the weights. You can run it, modify it, and build on it without restrictions.

The Cloud Trade-off Nobody Talks About

Cloud AI models trade your data for intelligence. Running locally means you keep your data — but until Gemma 4, it also meant a steep penalty in model quality. That trade-off is now gone.

When you send a query to a cloud model, it leaves your machine, travels through a network, hits a data center, processes on someone else’s hardware, and returns. Every hop is a dependency. Every dependency is a failure point. Legal, healthcare, and financial use cases often can’t send data to third-party APIs at all.

Running Gemma 4 locally means the data never leaves your hardware — and thanks to the benchmark numbers above, you’re no longer sacrificing frontier-level reasoning to get that guarantee.

The Problem We’re Solving

Goal: Deploy Gemma 4 on a consumer Intel i5 with exactly 16GB of RAM. No GPU. No cloud. No VRAM.

Standard PyTorch and HuggingFace pipelines won’t cut it here — they’re built for GPU flexibility, which makes them catastrophically inefficient when CPU-bound. To do this right, we need control at the metal level.

Our Stack

Layer Tool Why
Runtime Rust + Candle Zero interpreter overhead, direct memory control
SIMD Math AVX2 Process multiple values per clock cycle
Model Loading memmap2 Stream weights from disk, skip RAM spikes
KV Cache TurboQuant (3-bit) 6× smaller conversation memory
Thread Control core_affinity Eliminate cache misses from OS preemption
Model Format Quantized .safetensors Shrink 16GB model → ~4–5GB

Section 1: Drop Python. Load the Model in Rust.

Python is your biggest enemy on a 16GB machine. Its VM, garbage collector, and library ecosystem all eat RAM before your model even loads. The moment you spike past 16GB, your OS starts swapping to disk — and token generation speed drops to near zero.

The fix: Rust + Candle — Hugging Face’s lightweight ML framework for Rust with near-zero overhead.

Project Setup

cargo init gemma-on-cpu

Cargo.toml — note the avx feature flag:

[package]
name = "gemma-on-cpu"
version = "0.1.0"
edition = "2021"

[dependencies]
# The core ML engine — avx tells it to use CPU vector math
candle-core = { version = "0.8.2", features = ["avx"] }
# Maps the file into memory without loading it all at once
memmap2 = "0.9.3"

Loading Weights with Memory Mapping

Instead of reading the entire model into RAM at once, we memory-map the file. The OS pages in only what’s needed during computation.

// src/main.rs
use candle_core::{Device, safetensors};
use std::fs::File;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Candle auto-uses AVX because of our feature flag above
    let device = Device::Cpu;
    println!("Using device: {:?}", device);

    println!("Opening model file...");
    let file = File::open("gemma-4-quantized.safetensors")?;

    // Memory-map: the OS handles paging, we never spike RAM
    let mmap = unsafe { memmap2::MmapOptions::new().map(&file)? };

    let tensors = safetensors::load_buffer(&mmap, &device)?;
    println!("Loaded {} model tensors.", tensors.len());

    Ok(())
}

Why this works on 16GB:

No Python VM, no GPU drivers, no idle bloat. memmap2 keeps us well under the RAM ceiling. The avx feature flag routes math through the CPU’s native vector instructions, processing multiple values per clock cycle instead of one at a time.

Section 2: The Hidden Trap — The KV Cache

Loading the model is only half the battle. Here’s what catches most developers: the KV Cache.

Every token in your conversation history gets stored in this cache at 16-bit precision. For a model like Gemma 4, a long conversation can consume 4–5GB of RAM just for memory state. On a 16GB system, that’s a crash waiting to happen.

Enter TurboQuant

TurboQuant is a Rust implementation of PolarQuant and QJL, published at ICLR 2026 and available as turbo-quant on crates.io. It compresses the KV cache by ~6× — down to 3–4 bits — without meaningfully degrading output quality. It uses a two-step approach:

PolarQuant rotates the data and stores angles instead of raw coordinates. Angles are highly compressible because they are predictable and bounded.

QJL applies a 1-bit error checker that corrects drift introduced by compression.

Implementation

Update Cargo.toml:

[dependencies]
candle-core = { version = "0.8.2", features = ["avx"] }
candle-nn = "0.8.2"
turbo-quant = "0.1.0"
memmap2 = "0.9.3"

Initialize the compressed cache before inference:

use candle_core::Device;
use turbo_quant::TurboQuantCache;

// Inside main(), after loading tensors:
println!("Initializing TurboQuant KV Cache...");

// 3-bit compression — roughly 6× smaller than the default 16-bit cache
let bit_width = 3;

let mut kv_cache = TurboQuantCache::new(
    config.num_hidden_layers,
    config.num_attention_heads,
    config.head_dim,
    bit_width,
    &device
)?;

println!("3-bit KV cache ready.");

No matter how long the conversation runs, memory growth is now negligible.

Section 3: Stopping CPU Stutter with Thread Pinning

Side-by-side diagram of thread bouncing across cores versus pinned to one core

Even with efficient loading and compressed memory, generation may randomly stutter. The culprit is the OS scheduler.

The Kitchen Analogy

Think of each CPU core as a chef with a small prep counter (L1/L2 cache). Grabbing from the counter is instant. Grabbing from the walk-in fridge (RAM) is slow.

Windows will interrupt your AI thread mid-calculation to handle a background app, then resume it on a different core. That new core’s cache is cold. It has to refetch everything from RAM from scratch.

This is a cache miss, and it destroys throughput.

The Fix: Processor Affinity

Lock the AI thread to specific cores so the OS scheduler can’t migrate it.

[dependencies]
core_affinity = "0.8.1"
use core_affinity;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    println!("Locking CPU cores...");

    if let Some(core_ids) = core_affinity::get_core_ids() {
        // Pin the main thread to Core 0 — it stays there permanently
        if core_affinity::set_for_current(core_ids[0]) {
            println!("AI thread pinned to Core 0.");
        }
    }

    // ... proceed with device init and model loading

💡 For multi-threaded inference, spawn one thread per physical core and pin each independently.

Ditch the IDE at Runtime

VS Code consumes 500MB–1.2GB at idle. On a 16GB system, that’s not acceptable during inference.

Workflow: write and compile inside VS Code, run cargo build --release, close VS Code entirely, then launch via a bare batch file:

run_gemma.bat:

@echo off
echo =========================================
echo Starting Gemma 4 CPU Inference...
echo Close VS Code and other RAM-heavy apps first!
echo =========================================
pause

targetreleasegemma-on-cpu.exe

echo.
echo Inference complete.
pause

Section 4: Quantization — Fitting Gemma 4 into 16GB

MoE routing diagram showing 2 of 128 experts active per token

Here’s the core math problem: a model at 16-bit precision needs roughly 2GB per billion parameters. The 31B dense model at full precision would consume the entire 16GB budget before your OS even boots.

How Quantization Works

Think of it like measuring wood. You could measure to the nearest micrometer — or round to the nearest centimeter. Less precise, but far cheaper to store.

Quantization maps 16-bit floats → 4-bit or 8-bit integers:

Format Model Size RAM Left for System
16-bit (default) ~62 GB (31B model) impossible ❌
8-bit quantized ~31 GB still too large ❌
4-bit quantized ~15.5 GB tight but workable ✅
4-bit (26B MoE) ~13 GB comfortable ✅✅

The trade-off is minor quality degradation — in practice imperceptible for most use cases. This is why we load gemma-4-quantized.safetensors in Section 1, and why the 26B MoE is actually the better target for 16GB deployments — it has 26B worth of stored knowledge but only activates 3.8B parameters per token, so it runs faster and fits more comfortably in RAM than the dense 31B.

Putting It All Together

Here’s the full optimization stack and what each layer contributes:

[Gemma 4 Quantized Weights]  →  ~13–15 GB on disk (26B MoE or 31B)
        ↓ memmap2
[Candle / AVX2 Inference]    →  No Python overhead, SIMD math
        ↓ TurboQuant
[3-bit KV Cache]             →  6× less RAM per conversation turn
        ↓ core_affinity
[Thread-pinned CPU cores]    →  No cache misses, no OS preemption
        ↓ .bat launcher
[Clean RAM environment]      →  IDE closed, full budget for inference

Conclusion: You Don’t Need a $2,000 GPU

The industry narrative says local LLM deployment requires enterprise GPU hardware.

That’s objectively false — and Gemma 4’s benchmark numbers make it even more false than it was a year ago. A 26B MoE model that activates 3.8B parameters per token, scores 79.2% on GPQA Diamond, and outperforms OpenAI’s 120B model is not a compromise. It’s a legitimate choice.

By combining Rust + Candle over Python, AVX2 vector math, memmap2 for safe model loading, TurboQuant for KV cache compression, thread pinning to eliminate scheduler noise, and quantized Gemma 4 weights to fit the 16GB budget — you can run robust, private, offline inference on consumer silicon.

Hardware constraints aren’t roadblocks. They’re filters that demand better engineering.

Compile the release build. Close the IDE. Let the CPU do its job.

Running this on unusual hardware? Drop your setup in the comments — curious what people are squeezing inference out of.