Programming
-
The Human's Guide to the Command Line (macOS Edition)
If you’ve ever stared at a terminal window and felt like you were looking at the cockpit of an airplane, this post is for you. The command line doesn’t have to be scary, and I’m going to walk you through setting up a genuinely great terminal experience on your Mac, from scratch.
This is Part 1 of a Series. I will update the links here as I publish the other articles.
The Mental Model
Before we type anything, here’s the thing you need to understand: the terminal is just a text-based version of Finder.
When you see a folder in Finder, you double-click to open it. In the terminal, you type
cd(change directory) to enter it. You can run applications from Finder, and you can run applications from the terminal. It’s just a different way to do things you already understand.Once that clicks, everything else falls into place.
Part 1: The Foundation (Homebrew)
On a Mac, you install apps from the App Store. On the command line, we use Homebrew β it’s basically the App Store for the terminal. It’s a package manager that makes installing tools painless.
Let’s start by opening the terminal. Hit Command + Space, type “Terminal”, and hit Enter. Then paste this:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"It’ll ask for your Mac password. When you type it, nothing will appear on the screen. This is normal, it’s a security feature, not a bug. Just type your password and hit Enter.
Homebrew needs admin access because it creates system-level folders (usually
/opt/homebrew) and changes their ownership from the system account to your user account.Once it finishes, verify it’s working:
which brewIf that doesn’t return a path, run these two lines to add Homebrew to your shell:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile eval "$(/opt/homebrew/bin/brew shellenv)"Part 2: A Better Terminal (Ghostty)
Now that we have Homebrew, the first thing we’re going to install is a better terminal. The built-in Terminal app works, but Ghostty is faster, more configurable, and just nicer to use.
brew install --cask ghosttyOnce it finishes, find Ghostty in your Applications folder and open it. You can close the old Terminal app forever, congratulations, you’ve graduated.
Part 3: Making It Beautiful (Nerd Fonts & Starship)
Standard fonts don’t have icons for folders, git branches, or cloud status. We need a Nerd Font so the terminal can speak in pictures.
This is optional because Ghostty embeds a default font (JetBrains Mono) that has built-in “Nerd Font” support, but still helpful if you want to know how to change the font.
brew install --cask font-fira-code-nerd-fontPress Cmd + , to open Ghostty’s config file in your text editor. Add this line:
font-family = FiraCode Nerd FontSave the file, then reload config with Cmd + Shift + ,.
Install Starship
Starship is the paint job that turns a boring
$prompt into something colorful and actually helpful. It shows you what folder you’re in, what git branch you’re on, and more…brew install starshipWhile we’re at it, let’s install two plugins that make typing in the terminal way more pleasant. One whispers suggestions based on your command history, and the other color-codes your commands so you can spot typos before you hit Enter.
brew install zsh-autosuggestions zsh-syntax-highlightingWire It All Up
We need to tell your Mac to turn these features on every time you open a terminal. Copy and paste this entire block into Ghostty:
# Add Starship grep -qq 'starship init zsh' ~/.zshrc || echo 'eval "$(starship init zsh)"' >> ~/.zshrc # Add Auto-suggestions grep -qq 'zsh-autosuggestions.zsh' ~/.zshrc || echo "source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh" >> ~/.zshrc # Add Syntax Highlighting grep -qq 'zsh-syntax-highlighting.zsh' ~/.zshrc || echo "source $(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh" >> ~/.zshrc # Refresh your terminal source ~/.zshrcEach line checks if the setting already exists before adding it, so it’s safe to run more than once. When it finishes, your prompt should look completely different. Welcome to your CLI future.
Part 4: Basic Survival Commands
Now that your terminal looks proper, here’s how you actually move around.
Command Human Translation pwd“Where am I right now?” ls“Show me everything in this folder.” cd [folder]“Go inside this folder.” cd ..“Go back one folder.” clear“Wipe the screen and start fresh.” touch [file.txt]“Create a new blank file.” That’s about 90% of what you need to navigate around.
Two Golden Rules
1. Tab is your best friend. Type
cd Docand hit Tab β the terminal will finish the wordDocumentsfor you. This works for files, folders, and even commands. If there are multiple matches, hit Tab twice to see all the options.2. Ctrl + C is the panic button. If the terminal is doing something you didn’t expect, or a command is running and won’t stop, hold Ctrl and press C. It’s the emergency brake, and it’s always there for you.
A good next step: try navigating to your Desktop using only
cdandls. Find a file there, maybe create one withtouch, and then look at it withls. Once you can do that comfortably, you’ve officially mastered the basics./ Programming / Macos / Tutorial / Command-line
-
I Benchmarked JSON Parsing in Bun, Node, Rust, and Go
I’m just going to start posting about JSON everyday. Well ok, maybe not every day, but for the next few days at least. Later this week I’ve committed to writing a guide on getting started with CLIs for non-programmers, so stay tuned for that.
This morning I benchmarked JSON parsing across four runtimes: Bun, Node, Rust, and Go.
The Results
- Bun is the overall winner on large files β 307-354 MB/s, beating even Rust’s serde_json for untyped parsing
- Rust wins on small/nested data (225 MB/s small, 327 MB/s nested) due to low overhead
- Node is close behind Bun β V8’s JSON.parse is very optimized
- Go is ~3x slower than the JS runtimes on large payloads (encoding/json is notoriously slow)
- Memory: Bun reports 0 delta (likely GC reclaims before measurement), Rust’s tracking allocator shows the true heap cost (73-96MB), Go uses 52-65MB
Rust’s numbers were the most honest here since the tracking allocator catches everything. We should take Bun result with grain of salt because benchmarking memory in GC’d languages is tricky.
The json parser in v8 in node is the exact same as what is in Chrome…
Here’s the full test results if you want to dig into the numbers yourself.
More JSON content coming soon. You’ve been warned.
/ Programming / Bun / Node / Json / Benchmarks / Rust / Go
-
I Wrote Multiple CUE Parsers and Benchmarked Them Against JSON
It started yesterday because I wanted to fix JSON.
Well, not fix exactly. More like figure out if there was something better for client-side parsing in the browser. I’d been looking at comparing JSON versus MessagePack when I stumbled into CUE, a configuration language that embeds schema definitions directly in the data.
I posted about it and then was like well what if I just actually did it. There was just one problem: the core CUE spec is written in Go, which obviously doesn’t help when you need something running in React.
So I wrote a TypeScript parser for CUE. You can find it at cue-ts.
Then I built a benchmark app with 11 different parsing and deserialization approaches, all running client-side in a Vite + React app. We’ll see how that turned out here.
The Contenders
I tested across three ecosystems:
JSON: Native
JSON.parse, JSON + Zod validation, and JSON + Ajv (both pre-compiled and per-call schema compilation).CUE: Full AST parsing, a TypeScript deserializer, a fused single-pass scanner, pre-compiled CUE schemas, and even a Rust-to-WASM CUE parser.
Binary: MessagePack decoding, with and without Zod validation.
Payloads ranged from ~1KB to ~460KB across CUE, JSON, and MessagePack formats.
Understanding the Schema Problem
Before we look at numbers, we need to talk about schema validation.
With Zod, your schema is TypeScript code. It’s set at compile time, so there’s no runtime schema parsing overhead. With Ajv in compiled mode, it takes a JSON Schema document and generates an optimized JavaScript validation function. You do this once, and the runtime cost is essentially zero.
But CUE does something different. It embeds schema definitions directly in the data file:
#User: { email: string & =~"^[^@]+@[^@]+$" role: "admin" | "editor" | "viewer" age: int & >=0 } user: #User & { email: "[email protected]" role: "admin" age: 30 }Schema and data together, processed in one pass. Sounds elegant, right? The question is whether that elegance costs you speed. π
The Results
Here’s the 10KB payload benchmark on Chromium/V8:
Strategy Median vs JSON.parse JSON + Ajv (compiled) 18 ΞΌs ~equivalent JSON.parse 18 ΞΌs baseline JSON + Zod 28 ΞΌs 1.6x slower MsgPack Decode 30 ΞΌs 1.7x slower CUE (compiled schema) 112 ΞΌs 6.2x slower CUE Fast Deserialize 114 ΞΌs 6.3x slower CUE Deserialize (WASM) 755 ΞΌs 41.9x slower JSON + Ajv compiled is equivalent to bare
JSON.parse. The pre-compiled validator adds essentially zero overhead. Case closed?Not quite.
The Fair Fight
Those benchmarks aren’t comparing the same thing. Zod and Ajv compiled both pre-process their schemas before the benchmark runs. CUE processes its schema on every call. So I ran the fair comparison, schema processing included:
Strategy Median CUE Fast Deserialize 114 ΞΌs CUE (compiled schema) 112 ΞΌs JSON + Ajv (interpret) 4,840 ΞΌs When JSON has to compile its schema per call, CUE is 43x faster. Cool? All that work really paid off?
Well, the bottleneck for CUE isn’t schema processing. It’s parsing the text format itself. I tried basically every optimization I could think of. The deserializer spends nearly all its time scanning characters, building strings and numbers. Without native C++ code, which is what
JSON.parsegets for free from the browser engine, a TypeScript parser just can’t close that gap.MessagePack: Not Worth It
I included MessagePack benchmarks to see if smaller binary payloads would translate to faster parsing. They don’t. MessagePack is consistently slower than
JSON.parse, even with smaller payloads. The overhead of maintaining a separate binary serialization format on the client side just isn’t worth it for most use cases.Cross-Browser Highlights
I ran a few tests across engines. Some notable findings:
- Safari (JSC): Zod validation is essentially free. JSON + Zod matches bare
JSON.parse - Chrome (V8): CUE’s fast deserializer benefits from
charCodeAtoptimizations - Firefox (SpiderMonkey): Most consistent results across the board
So What’s the Answer?
If you can compile your schema ahead of time with Ajv or Zod, do that and use JSON.
It’s not even close. Seriously.
JSON.parsebenefits from decades of native browser optimization that no TypeScript parser can match, and pre-compiled validation adds essentially zero overhead.CUE is faster in one specific scenario: when you need to process schemas dynamically on every call. 43x faster than per-call Ajv compilation is real, but how often does that actually come up in production? It’s hard to say.
Use Case Pick This Why Hot-path APIs JSON + Ajv (compiled) Fastest possible TypeScript projects JSON + Zod Best DX Dynamic schemas CUE 43x faster than per-call Ajv Config files CUE Single source of truth Bandwidth-constrained MsgPack 30-60% smaller payloads One thing I considered but didn’t build for this post: a conversion layer that stores everything as CUE on the server, then compiles it down to JSON with Zod or Ajv schemas for the browser. You’d get CUE’s authoring experience with JSON’s runtime speed. That feels like an interesting project, and maybe I’ll get to it next.
Both the cue-ts parser and the benchmark app are open source. Try them yourself and let me know what you think.
/ Programming / Webdev / javascript / Benchmarks
- Safari (JSC): Zod validation is essentially free. JSON + Zod matches bare
-
Is There Something Better Than JSON?
Have you ever looked at a JSON file and thought, “There has to be something better than this”? I have.
JSON has served us well. It works with everything, and it’s human readable. It’s a decent default, don’t get me wrong, but the more you use it, you’ll find its limitations to be quite painful. So before we answer the question of whether there’s anything better, we should describe what’s actually wrong with JSON.
The Problems with JSON
First, there’s no type system. No datetimes, no real integers, no structs, no unions, no tuples. If you need types, and you almost always do, you’re on your own.
Second, JSON is simple, which sounds like a feature until you try to store anything complicated in it. You end up inventing your own schema, and the schema tooling out there (JSON Schema, etc.) gets verbose fast. Because the spec is so loose, validation can be inconsistent across implementations.
There’s more: fields can be reordered, you have to receive the entire document before you can start verifying it, and there are no comments. You can’t leave a note for the next person explaining why a config value is set a certain way. That’s a real problem for anything that lives in version control.
The Machine-Readable Alternatives
Now, there are plenty of binary serialization formats that solve some of these issues. Protobuf, Cap’n Proto, CBOR, MessagePack, BSON. They’re all interesting and have their place. But they’re machine readable, not human readable. You can’t just open one up in your editor and make sense of it. So let’s set those aside.
The question I’m more interested in is: is there something better than JSON that you can still read and edit as a text file?
It turns out there are two solid options.
Dhall
Dhall is a programmable configuration language. Think of it as JSON with all the things you wish JSON had: functions, types, and imports. You can convert JSON to Dhall and back, and it’s just a text file you can open in any editor. The name comes from a character in an old video game, and the language itself is interesting enough that it’s worth your time to explore.
CUE
CUE stands for Configure, Unify, and Execute. It’s similar to Dhall in that it fills the gaps JSON leaves behind, like types, validation, and constraints, while staying human readable. Where CUE really pulls ahead is in its feature set. You can import Protobuf definitions, generate JSON Schema, validate existing configs, and a lot more. In terms of raw capabilities, CUE has more going on than Dhall.
JSON isn’t going anywhere. But if you’re looking for something interesting to explore, check out both of these. They make great fun little side projects.
/ DevOps / Programming / Json / Configuration
-
The Death of Clever Code
One positive product of working with Agentic tools is they rarely suggest clever code. No arcane one-liners, no “look how smart I am” abstractions. And, well, I’m here for it.
Before we continue it helps to understand a bit about how LLMs work. These models are optimized for pattern recognition. They’ve been trained on massive amounts of code and learned what patterns appear most frequently.
Clever code, by definition, is bespoke. It’s the unusual pattern, the one-off trick. There just isn’t enough training data for cleverness. The AI gravitates toward the common, readable solution instead.
Let me give you an example.
Show Me the Code
Here’s a nested ternary:
const result = a > b ? (c > d ? 'high' : 'mid') : (e > f ? 'low' : 'none');I’d be impressed if you could explain that correctly on your first try. What happens when there’s a bug in one of those conditions? Good luck debugging that.
Now here’s the same logic:
let result; if (a > b) { if (c > d) { result = 'high'; } else { result = 'mid'; } } else { if (e > f) { result = 'low'; } else { result = 'none'; } }A lot easier, right? If it’s easy to read, it’s easy to maintain. The AI tooling doesn’t struggle to read either version, but you might, and when there is a bug, explaining exactly what needs to change becomes the hard part.
Actually wait. It turns out, not all complexity is created equal.
Two Kinds of Complexity
Essential complexity is the complexity of the problem itself. If you’re building a mortgage calculator or doing tax calculations, there’s inherent complexity in understanding the domain. You can’t simplify that away, and you shouldn’t try.
Accidental complexity is the stuff you introduce. The nested ternary instead of the if/else. Five layers of abstraction for the sake of abstraction that only runs in a specific edge case. Generic utility functions where you’ve tried to cover every possible scenario, but realistically you only need two or three cases.
Ok but what about abstraction, since abstraction is where accidental complexity loves to hide?
Good Abstraction vs. Bad Abstraction
Abstraction shows up everywhere in programming, but let’s think about it in two flavors.
Good abstraction hides details the caller doesn’t need to care about. The interface clearly communicates what it does. Think
array.sort(), you look at it and immediately know what’s happening. Those dang arrays getting some sort of sorted. You know exactly what it does without caring about the implementation.Bad abstraction hides details you do need to understand in order to use it correctly. Think of a
processData()method that’s doing six different things with an internal state that’s nearly impossible to test. And splitting it intoprocessData1()throughprocessData6()doesn’t help either. That’s just moving the vegetables around on your plate which doesn’t mean you’ve actually finished dinner.AI Signals
So why does any of this matter for working with AI coding tools?
Because if the agents keep getting your code wrong, if they consistently misunderstand what a function does or there are incorrect modifications, that’s a signal.
It’s telling you that your code has some flavor of cleverness that makes it hard to reason about. Not just for the AI, but for your team, and for you six months from now.
The goal is to code where the complexity comes from the problem, not from the solution. The AI struggling with your code is like a canary in the coal mine for maintainability.
/ AI / Programming / Code-quality
-
From SUnit to Vitest: A Brief History of JavaScript Testing
I care a lot about testing. I don’t know if that’s obvious yet, but hopefully it’s obvious. I wanted to trace the lineage of the testing tools we use today, because I think understanding where they came from helps you appreciate why things work the way they do.
Where It All Started
Automated testing as we know it really started with SUnit for the Smalltalk language. Kent Beck created it back in 1989, and it established the patterns that every test framework still borrows from today.
In 1997, Kent Beck and Erich Gamma ported those ideas to Java and created JUnit. JUnit was, or is, incredibly influential to pretty much every unit testing framework you’ve ever used. The test runner, assertions, setup and teardown, all of that traces back to JUnit.
But I’m going to focus on the JavaScript side of things here.
Jest: The Facebook Era
Jest was originally created at Facebook in 2011 as part of a major platform rewrite. It became the dominant testing framework for React and Node.js codebases, and in 2022, Facebook released it to the OpenJS Foundation.
Jest works well, but it carries some baggage. It requires a transpilation pipeline, something that was common a decade ago but feels burdensome now. If you want to use ESM modules, there’s an extra step involved. It’s adds friction.
So what else is there?
Vitest: The Modern Alternative
Vitest is a modern alternative, built on top of Vite. It supports ESM modules and TypeScript out of the box; so no transpilation step needed. And because Vite has HMR (hot module replacement), the watch mode for rerunning tests is very fast.
Vitest was initially created by Anthony Fu and the company behind Vue.js. The initial commit was in December 2021, so it’s a relatively recent project. They’ve made incredible progress since then.
Vitest uses Vite under the hood. So lets look at that briefly.
Why Vite Is So Fast
Vite’s speed comes from esbuild, which is written in Go. It compiles directly to native machine code, so it bypasses the JavaScript engine’s overhead entirely. It can transform TypeScript significantly faster because it doesn’t need to go through the JS engine. And because it’s Go, it’s multithreaded.
But things are changing. In Vite 8, the bundler is moving from esbuild to Rolldown. This is new tool written in Rust that combines the best of esbuild and Rollup.
Why? Currently, Vite uses esbuild during development but switches to Rollup for production builds. Two different tools for two different use cases. Rolldown unifies both into a single tool that handles dev and production.
What Did We Learn?
Hopefully something! How about a mini review to nail it home:
- 1989: SUnit (Smalltalk) β Kent Beck starts it all
- 1997: JUnit (Java) β the template everything else follows
- 2011: Jest β Facebook’s testing framework, now under OpenJS Foundation
- 2021: Vitest β modern, fast, ESM-native testing built on Vite
- Coming soon: Rolldown replaces esbuild + Rollup in Vite 8
That’s all for now!
/ Programming / Testing / javascript
-
Garbage Collection: How Python, JavaScript, and Go Clean Up After Themselves
It’s Garbage day for me IRL and I wanted to learn more about garbage collection in programming. So guess what? Now you get to learn more about it too.
We’re going to focus on three languages I work with mostly, Python, JavaScript, and Go. We will skip the rest for now.
Python: Reference Counting
Python’s approach is the most straightforward of the three. Every object keeps track of how many things are pointing to it. When that count drops to zero, the object gets cleaned up immediately. Simple.
There’s a catch, though. If two objects reference each other but nothing else references either of them, the count never hits zero. That’s a reference cycle, and Python handles it with a secondary cycle detector that periodically scans for these orphaned clusters. But for the vast majority of objects, reference counting does the job without any fancy algorithms.
JavaScript (V8): Generational Garbage Collection
Most JavaScript you encounter is running on V8… so Chrome based browsers, Node and Deno. V8 uses a generational strategy based on a simple observation: most objects die young.
V8 splits memory into two main areas:
- The Nursery (Young Generation): New objects land here. This space is split into two halves and uses a scavenger algorithm. It’s fast because it only deals with short-lived variables β and most variables are short-lived.
- Old Space (Old Generation): No, not the deodorant company. Objects that survive a couple of scavenger rounds get promoted here. Old space uses a mark-and-sweep algorithm, which is slower but handles long-lived objects more efficiently.
First it asks, “How long has this object been around?” New stuff gets the quick treatment, and anything that sticks around gets put out to the farm, to be delt where time is less of a premium. It’s a smart tradeoff between speed and memory efficiency.
Go: Tricolor Mark-and-Sweep
Go’s garbage collector also uses mark-and-sweep, but with a twist called tricolor marking. Here’s how it works:
- White objects: These might be garbage but haven’t been checked yet. Everything starts as white.
- Gray objects: The collector has reached these, but it hasn’t scanned their children (the things they reference) yet.
- Black objects: These are confirmed alive β the collector has scanned them and all their references.
The collector starts from known root objects, marks them gray, then works through the gray set β scanning each object’s references and marking them gray too, while the scanned object itself turns black. When there are no more gray objects, anything still white is unreachable and gets cleaned up.
Go’s approach is notable because it runs concurrently. This helps with latency while the GC is running.
Garbage, Collected
Each approach reflects the language’s priorities:
- Python optimizes for simplicity and predictability. Objects get cleaned up correctly when they’re no longer needed
- JavaScript optimizes for speed in interactive applications. Quick cleanup for short-lived objects, thorough cleanup for the rest
- Go optimizes for low latency. Concurrent collection is great for server side processes
References
- Design of CPython’s Garbage Collector β Python Developer’s Guide deep dive into reference counting and cycle detection
- Orinoco: Young Generation Garbage Collection β V8’s parallel scavenger and generational GC design
- A Guide to the Go Garbage Collector β Official Go documentation on the concurrent tricolor collector
/ Programming / Golang / Python / javascript / Garbage-collection
-
How Smart People Are Using Claude Code Skills to Automate Anything
π Build agentic systems that run your business: skool.com/scrapes Don’t miss the next build - www.youtube.com/@simonscr…
/ Programming / Claude / links / code
-
Everything Is Eventually a Database Problem
I think there’s a saying that goes something like “code is ephemeral, but data is forever.” That’s never been more true than right now. Code is easier than ever to create, anyone can spin up a working app with an AI agent and minimal experience. But your data structure? That’s the thing that sticks around and haunts you.
Data modeling is one of those topics that doesn’t get enough attention, especially given how critical it is. You need to understand how your data is stored, how to structure it, and what tradeoffs you’re making in how you access it. Get it right early, and your code stays elegant and straightforward. Get it wrong, and your codebase becomes a forever series of workarounds…
Microservices Won’t Save You
For teams moving from a monolith to microservices, if the data stays tightly coupled, you don’t really have microservices; you have a distributed monolith with extra network hops.
Yes, data can be coupled just like code can be coupled. If all your different services are still hitting the same database with the same schema, you have a problem. You need separate data structures for your services, not a monolithic architecture hiding behind a microservices facade.
The Caching Trap
So what happens when you have a lot of data and your queries get slow? You’ve done all the easy stuff; optimized queries, added indexes, followed best practices. But things are still slow.
Every senior engineer’s first instinct is the same: “Let’s add Redis in front of it.” Or “more read replicas.” And sure, that works, but you have just added complexity and now you have to deal with cache invalidation.
What happens when you have stale data? How do you recache current data, and when does that happen?
Are you caching on the browser side too? Understanding where data can be cached and how to invalidate it is another genuinely difficult problem to solve. You’re just trading one set of problems for a different set of problems.
You Can’t Predict Every Future Question
If you’re selling things on the internet, chances are, you will care about event sourcing at some point. A lot of interesting business problems don’t care about the current state of a user, they care about the intent and history. So how you store intent and history is probably different from your ACID-compliant Postgres table that you’ve worked hard to normalize.
You can get your data structure perfect for displaying products and processing sales, then run into a completely new set of requirements that changes everything about how your data needs to be structured.
It’s genuinely hard to foresee all the potential questions you’ll need to answer in the future.
Why This Matters Now
Everything you do on a computer stores data somewhere, it’s just a matter of persistence.
Which is why; everything software-related is eventually a database problem.
Data modeling isn’t glamorous, but getting it right is the difference between a system that scales gracefully and one that fights you every step of the way.
/ Programming / Databases / Architecture
-
Your Context Window Is a Budget β Here's How to Stop Blowing It
If you’re using agentic coding tools like Claude Code, there’s one thing you should know by now: your context window is a budget, and everything you do spends it.
I’ve been thinking about how to manage the budget. As we are learning how to use sub-agents, MCP servers, and all these powerful capabilities we haven’t been thinking enough about the cost of using them. Certainly the dollars and cents matters too if you are using API access, but the raw token budget you burn through in a single session impacts us all regardless. Once it’s gone, compaction kicks in, and it’s kind of a crapshoot on whether it knows how to pick up where we left off on the new session.
Before we talk about what you can do about it, let’s talk about where your tokens go, or primarily are used.
Why Sub-Agents Are Worth It (But Not Free)
Sub-agents are one of the best things to have in agentic coding. The whole idea is that work happens in a separate context window, leaving your primary session clean for orchestration and planning. You stay focused on what needs to change while the sub-agent figures out how.
Sub-agents still burn through your session limits faster than you might expect. There are actually two limits at play here:
- the context window of your main discussion
- the session-level caps on how many exchanges you can have in a given time period.
Sub-agents hit both. They’re still absolutely worth using and working without them isn’t an option, but you need to be aware of the cost.
The MCP Server Problem
MCP servers are another area where things get interesting. They’re genuinely useful for giving agentic tools quick access to external services and data. But if you’ve loaded up a dozen or two of them? You’re paying a tax at the start of every session just to load their metadata and tool definitions. That’s tokens spent before you’ve even asked your first question.
My suspicion, and I haven’t formally benchmarked this, is that we’re headed toward a world where you swap between groups of MCP servers depending on the task at hand. You load the file system tools when you’re coding, the database tools when you’re migrating, and the deployment tools when you’re shipping. Not all of them, all the time.
There’s likley more subtle problems too. When you have overlapping MCP servers that can accomplish similar things, the agent could get confused about which tool to call. It might head down the wrong path, try something that doesn’t work, backtrack, and try something else. Every one of those steps is spending your token budget on nothing productive.
The Usual Suspects
Beyond sub-agents and MCP servers, there are the classic context window killers:
- Web searches that pull back pages of irrelevant results
- Log dumps that flood your context with thousands of lines
- Raw command output that’s 95% noise
- Large file reads when you only needed a few lines
The pattern is the same every time: you need a small slice of data, but the whole thing gets loaded into your context window. You’re paying full price for information you’ll never use.
And here’s the frustrating part β you don’t know what the relevant data is until after you’ve loaded it. It’s a classic catch-22.
Enter Context Mode
Somebody (Mert KΓΆseoΔlu - mksglu) built a really clever solution to this problem. It’s available as a Claude Code plugin called context-mode. The core idea is simple: keep raw data out of your context window.
Instead of dumping command output, file contents, or web responses directly into your conversation, context-mode runs everything in a sandbox. Only a printed summary enters your actual context. The raw data gets indexed into a SQLite database with full-text search (FTS5), so you can query it later without reloading it.
It gives Claude a handful of new tools that replace the usual chaining of bash and read calls:
- ctx_execute β Run code in a sandbox. Only your summary enters context.
- ctx_execute_file β Read and process a file without loading the whole thing.
- ctx_fetch_and_index β Fetch a URL and index it for searching, instead of pulling everything into context with WebFetch.
- ctx_search β Search previously indexed content without rerunning commands.
- ctx_batch_execute β Run multiple commands and search them all in one call.
There are also slash commands to check how much context you’ve saved in a session, run diagnostics, and update the plugin.
The approach is smart. All the data lives in a SQLite FTS5 database that you can index and search, surfacing only the relevant pieces when you need them. If you’ve worked with full-text search in libSQL or Turso, you’ll appreciate how well this maps to the problem. It’s the right tool for the job.
The benchmarks are impressive. The author reports overall context savings of around 96%. When you think about how much raw output typically gets dumped into a session, it makes sense. Most of that data was never being used anyway.
What This Means for Your Workflow
I think the broader lesson here is that context management is becoming a first-class concern for anyone doing serious work with agentic tools. It’s not just about having the most powerful model, it’s about using your token budget wisely so you can sustain longer, more complex sessions without hitting the wall.
A few practical takeaways:
- Be intentional about MCP servers. Load what you need, not everything you have.
- Use sub-agents for heavy lifting, but recognize they cost session tokens.
- Avoid dumping raw output into your main context whenever possible.
- Tools like context-mode can dramatically extend how much real work you get done per session.
We’re still early in figuring out the best practices for working with these tools. But managing your context window? That’s one of the things that separates productive sessions from frustrating ones.
Hopefully something here saves you some tokens.
/ AI / Programming / Developer-tools / Claude
-
I got tired of plaintext .env files, so I built LSM.
lsm execwill inject secrets at runtime so they never touch the filesystem. Doppler’s idea, minus the monthly bill.How are you managing local secrets?
/ Programming / Tools / security
-
I switched to mise for version management a month ago. No regrets. No more
brew upgradebreaking Python. Built-in task runner replaced some of projects that were using Makefiles.Still juggling nvm + pyenv + rbenv?
/ DevOps / Programming / Tools
-
I wrote about why you should stop using pip. Poetry or uv. Pick one. pip has no lockfile, no dependency resolution worth trusting, and no isolation by default.
Have you moved to uv yet? Still happy with poetry? How’s it going?
/ Programming / Tools / Python
-
I wrote about when to reach for Python over Bash. Most veterans put the cutoff at 50 lines. If you’re writing nested if statements in Bash, you’ve already gone too far.
Where does Go fit in though? Overkill for scripts?
/ Programming / Python / Bash
-
With AI agents, the cost of writing tests is approaching zero. Five words: “write tests on new code.” The excuses are running out.
Are you verifying the tests your AI writes, or just trusting the green checkmarks?
/ Programming / Testing / Claude-code
-
I compared npm, Yarn, pnpm, and Bun. TLDR version: pnpm wins for most teams, Bun wins if you’re already on the runtime.
Has anyone switched their whole team to Bun yet? How’d that go?
/ Programming / Tools / javascript
-
I wrote about securing node_modules. Socket, Snyk, Dependabot β each catches different things. Hopefully answering when to use AI to rewrite simple deps you barely use.
Anyone want to build that CLI?
/ Programming / security / javascript
-
Wrote a guide on writing a good CLAUDE.md. Takeaway: keep it under 200 lines. Every line loads into context every session, so bloat costs real tokens.
How are you handling multiple AI context files across tools?
/ Programming / Tools / Claude-code
-
When to Use Python Over Bash
When to use python over bash is really a question of when to use bash. Python is a general-purpose language that can handle just about anything you throw at it. Bash, on the other hand, has a very specific sweet spot. Once you understand that sweet spot, the decision makes itself.
What Bash Actually Is
Bash is an interactive command interpreter and scripting language, created in 1989 for the GNU project as a free software alternative to the Bourne shell. It pulled in advanced features from the Korn shell and C shell, and it’s been commonly used by Unix and Linux systems ever since.
What makes Bash unique is its approach to data flow programming. Files, directories, and system processes are treated as first-class objects. Bash is designed to take advantage of utilities that almost always exist on Unix-based systems. So think of tools like
awk,sed,grep,cat, andcurl. Another important thing to know when writing effective Bash scripts, you also need to understand the pipeline operator and how I/O redirection works.A good Bash script will look something like this:
#!/bin/bash set -euo pipefail LOG_DIR="/var/log/myapp" DAYS_OLD=30 find "$LOG_DIR" -name "*.log" -mtime +"$DAYS_OLD" -print0 | xargs -0 gzip -9 echo "Compressed logs older than $DAYS_OLD days"Simple, portable, does one thing well. That’s Bash at its best.
Where Bash Falls Short
Bash isn’t typed. There’s no real object orientation. Error handling is basically
set -eand hoping for the best. There’s notry/catch, no structured exception handling. When things go wrong in a Bash script, they tend to go wrong quietly or spectacularly, with not much in between.Python, by contrast, is optionally strongly typed and object-oriented. If you want to manipulate a file or a system process in Python, you wrap that system entity inside a Python object. That adds some overhead, sure, but in exchange you get something that’s more predictable, more secure, and scales well from simple scripts to complex logic.
Here’s that same log compression task in Python:
from pathlib import Path import gzip import shutil from datetime import datetime, timedelta log_dir = Path("/var/log/myapp") cutoff = datetime.now() - timedelta(days=30) for log_file in log_dir.glob("*.log"): if datetime.fromtimestamp(log_file.stat().st_mtime) < cutoff: with open(log_file, "rb") as f_in: with gzip.open(f"{log_file}.gz", "wb") as f_out: shutil.copyfileobj(f_in, f_out) log_file.unlink()More verbose? Absolutely. But also more explicit about what’s happening, easier to extend, and much easier to add error handling to.
The Performance Question
In some cases, performance genuinely matters. Think high-frequency trading platforms, edge devices, or massive clusters. Bash scripts excel here because there’s almost zero startup overhead. Compare that to Python, which needs to load up the interpreter before it can start executing code. You’re going from microseconds to milliseconds, and sometimes that matters.
But startup time is just one factor. When you compare the actual work being done, Python can pull ahead. String manipulation on structured data? Python wins. Parsing JSON, YAML, or any structured format? Python’s core libraries are written in C and optimized for exactly this kind of work. If you find yourself reaching for
jqoryqin a Bash script, that’s a strong signal you should be using Python instead.The Guidelines People Throw Around
You’ll see a common guideline online: if your script exceeds 100 lines of Bash, rewrite it in Python. But a lot of veterans in the industry feel like that cutoff is way too generous. Experienced engineers often put it at 50 lines, or even 25.
Another solid indicator: nested
ifstatements. Some people say “deeply nested” if statements, but let’s be honest, more than one level of nesting in Bash is already getting painful. Python handles complex branching logic far more gracefully, and you’ll thank yourself when you come back to maintain it six months later.Unit Testing Tells the Story
You can do unit testing with Bash. BATS (Bash Automated Testing System) exists, and ShellCheck is useful as a lightweight linter for catching bad practices. But despite these tools, Python’s testing ecosystem is on another level entirely. It’s fully mature with multiple frameworks, excellent mocking capabilities, and the ability to simulate network calls, external APIs, or system binaries. Complex mocking that would be difficult or impossible in Bash is straightforward in Python.
If your script needs solid testing or if it’s doing anything important, that’s a strong vote for Python.
Bash’s Biggest Win: Portability
So what does Bash actually win at? Portability. When you think about all the dependencies Python needs to run, Bash is the clear winner. You’re distributing a single
.shfile. That’s it.With Python, you have to ask: Does Python exist on this machine? Is it the right version? You’ll need a virtual environment so you don’t pollute system Python. You need third-party libraries installed via a package manager; and please friends, remember that we don’t let friends use pip. Use Poetry or uv. Pip is so bad that I’d honestly argue that Bash not having a package manager is better than Python having pip. At least Bash doesn’t pretend to manage dependencies well.
If you want something simple, something that can run on practically any Unix-based machine without setup, Bash is your answer. Even Windows can handle it these days through WSL, though you’re jumping through a few hoops.
TLDR
The decision is actually pretty straightforward:
- Use Bash when you’re gluing together system commands, the logic is linear, it’s under 50 lines, and portability matters.
- Use Python when you’re parsing structured data, need error handling, have branching logic, want proper tests, or the script is going to grow.
If you’re reaching for
jq, writing nestedifstatements, or the script is getting long enough that you’re losing track of what it does… it’s time for Python.I think in a future post we might look at when Go makes sense over Bash. There’s a lot to cover there about compiled binaries, but for now, hopefully this helps you make the call next time you’re wondering what to start your scripting with.
/ DevOps / Programming / Python / Bash / Scripting
-
Local Secrets Manager - Dotenv Encrypter
I built a thing to solve a problem. It has helped me, maybe it will help you?
It all starts with a question.
Why isn’t there a good local secrets manager that encrypts your secrets at rest? I imagine a lot of people, like me, have a number of local applications. I don’t want to pay per-seat pricing just to keep my sensitive data from sitting in plaintext on my machine.
I built an app called LSM Local Secrets Manager to solve that problem. The core idea is simple. Encrypt your
.envfiles locally and only decrypt when you need them (sometimes at runtime).The Problem
If you’ve got a bunch of projects on your machine, each with their own
.envor.env.localfile full of API keys you’re definitely not rotating every 90 days. Those files just sit there in plaintext. Any process on your system can read them. And with AI agents becoming part of our dev workflows, the attack surface for leaking secrets is only getting easier.ThE CLAW EnteRed ChaT
I started looking at Doppler specifically for OpenCLAW. Their main selling feature is injecting secrets into your runtime so they never touch the filesystem. I was like, cool. Also I like that Doppler stores everything remotely. The only thing was the cost did not make sense for me right now. I don’t want to pay $10-20 a month for this set of features.
So what else is there?
Well GCP Secret Manager has its own set of issues.
You can’t have duplicate names per project, so something as common as
NODE_ENVacross multiple apps becomes a more work than you want to deal with. Some wrapper script that injects prefixes? No thanks. I imagine there are a thousand and one homegrown solutions to solve this problem. Again, no thanks.So what else is there?
You Find A Solution
AWS Secret Manager
A Problem for Solution Problem
AWS IAM
π«£
I have a lot more to say here on this subject but will save this for another post. Subscribe if you want to see the next post.
The Solution
The workflow is straightforward:
lsm initβ Run this once from anywhere. It generates your encryption key file.lsm link <app-name>β Run this inside your project directory. It creates a config entry in~/.lsm/config.yamlfor that application.lsm importβ Takes your existing.envor.env.localand creates an encrypted version.lsm cleanβ Removes the plaintext.envfiles so they’re not just sitting around.lsm dumpβ Recreates the.envfiles if you need them back.
But wait there’s more.
Runtime Injection with
lsm execRemember that cool thing I just told you about? Instead of dumping secrets back to disk, you run:
lsm exec -- pnpm devI feel like a family man from Jersey, who don’t mess around. Aye, you got, runtime injection. I got that.
Well that’s
lsmanyways. It can decrypt your secrets and inject them directly into the runtime environment of whatever command follows the--. Your secrets exist in memory for the duration of that process and nowhere else. No plaintext files hanging around for other processes to sniff.Credit to Doppler for the idea. The difference to what we are doing is your encrypted files stay local.
What’s Next
I’ve got some possible ideas of improvements to try building.
- Separate encrypt/decrypt keys β You create secrets with one key, deploy the encrypted file to a server, and use a read-only key to decrypt at runtime. The server never has write access to your secrets.
- Time-based derivative keys β Imagine keys that expire or rotate automatically.
- Secure sharing β Right now you’d have to decrypt and drop the file into a password manager to share it. There’s room to make that smoother.
I’m not sure how to do all of that yet, but we’re making progress.
Why Not Just Use Doppler?
There are genuinely compelling reasons to use Doppler or similar services. I mean bsides the remote storage, access controls and auditable logs. There’s a lot to love.
For local development across a bunch of personal projects? I don’t think you should need a SaaS subscription to keep your secrets encrypted.
LSM is still early, but the core workflow is there and it works.
Give it a try if you’re tired of plaintext
.envfiles scattered across your machine.
/ DevOps / Programming / Tools / security
-
Python: If your CLI tool uses print() and gets called as a subprocess by Claude Code, the output now gets swallowed. The parent process captures it. Structured logging will fix it.
/ Programming / Claude-code / Debugging
-
The Only Code That Defies Entropy
A developer explores the Second Law of Thermodynamics and finds that love is the only code capable of creating local order against universal entropy.
/ Programming / links / code / learning
-
How Python and Kotlin provide structured concurrency out of the box while Go achieves the same patterns explicitly using errgroup, WaitGroup, and context.
/ Programming / Golang / Python / links
-
How to Write a Good CLAUDE.md File
Every time you start a new chat session with Claude Code, it’s starting from zero knowledge about your project. It doesn’t know your tech stack, your conventions, or where anything lives. A well-written
CLAUDE.mdfile fixes that by giving Claude the context it needs before it writes a single line of code.This is context engineering, and your
CLAUDE.mdfile is one of the most important pieces of it.Why It Matters
Without a context file, Claude has to discover basic information about your project β what language you’re using, how the CLI works, where tests live, what your preferred patterns are. That discovery process burns tokens and time. A good
CLAUDE.mdfront-loads that knowledge so Claude can get to work immediately.If you haven’t created one yet, you can generate a starter file with the
/initcommand. Claude will analyze your project and produce a reasonable first draft. It’s a solid starting point, but you’ll want to refine it over time.The File Naming Problem
If you’re working on a team where people use different tools: Cursor has its own context file, OpenAI has theirs, and Google has theirs. You can easily end up with three separate context files that all contain slightly different information about the same project. That’s a maintenance headache.
It would be nice if Anthropic made the filename a configuration setting in
settings.json, but as of now they don’t. Some tools like Cursor do let you configure the default context file, so it’s worth checking.My recommendation? Look at what tools people on your team are actually using and try to standardize on one file, maybe two. I’ve had good success with the symlink approach , where you pick your primary file and symlink the others to it. So if
CLAUDE.mdis your default, you can symlinkAGENTS.mdorGEMINI.mdto point at the same file.It’s not perfect, but it beats maintaining three separate files with diverging information.
Keep It Short
Brevity is crucial. Your context file gets loaded into the context window every single session, so every line costs tokens. Eliminate unnecessary adjectives and adverbs. Cut the fluff.
A general rule of thumb that Anthropic recommends is to keep your
CLAUDE.mdunder 200 lines. If you’re over that, it’s time to trim.I recently went through this exercise myself. I had a bunch of Python CLI commands documented in my context file, but most of them I rarely needed Claude to know about.
We don’t need to list every single possible command in the context file. That information is better off in a
docs/folder or your project’s documentation. Just add a line in yourCLAUDE.mdpointing to where that reference lives, so Claude knows where to look when it needs it.Maintain It Regularly
A context file isn’t something you write once and forget about. Review it periodically. As your project evolves, sections become outdated or irrelevant. Remove them. If a section is only useful for a specific type of task, consider moving it out of the main file entirely.
The goal is to keep only the information that’s frequently relevant. Everything else should live somewhere Claude can find it on demand, not somewhere it has to read every single time.
Where to Put It
Something that’s easy to miss: you can put your project-level
CLAUDE.mdin two places../CLAUDE.md(project root)./.claude/CLAUDE.md(inside the.claudedirectory)
A common pattern is to
.gitignorethe.claude/folder. So if you don’t want to check in the context file β maybe it contains personal preferences or local paths β putting it in.claude/is a good option.Rules Files for Large Projects
If your context file is getting too large and you genuinely can’t cut more, you have another option: rules files. These go in the
.claude/rules/directory and act as supplemental context that gets loaded on demand rather than every session.You might have one rule file for style guidelines, another for testing conventions, and another for security requirements. This way, Claude gets the detailed context when it’s relevant without bloating the main file.
Auto Memory: The Alternative Approach
Something you might not be aware of is that Claude Code now has auto memory, where it automatically writes and maintains its own memory files. If you’re using Claude Code frequently and don’t want to manually maintain a context file, auto memory can be a good option.
The key thing to know is that you should generally use one approach or the other. If you’re relying on auto memory, delete the
CLAUDE.mdfile, and vice versa.Auto memory is something I’ll cover in more detail in another post, but it’s worth knowing the feature exists. Just make sure you enable it in your
settings.jsonif you want to try it.Quick Checklist
If you’re writing or revising your
CLAUDE.mdright now, here’s what I’d focus on:- Keep it under 200 lines β move detailed references to docs
- Include your core conventions β package manager, runtime, testing approach
- Document key architecture β how the project is structured, where things live
- Add your preferences β things Claude should always or never do
- Review monthly β cut what’s no longer relevant
- Consider symlinks β if your team uses multiple AI tools
- Use rules files β for detailed, task-specific context
That’s All For Now. π
/ AI / Programming / Claude-code / Developer-tools
-
aden-hive/hive: Outcome driven agent development framework that evolves
Outcome driven agent development framework that evolves - aden-hive/hive
/ Programming / links / agent / platform