I remember the moment I realized my favorite CLI tool had turned against me. The release notes promised faster builds, better caching, and a dozen new flags I could pass to tweak internal behavior. What I got instead was a configuration file that ballooned from twenty lines to over two hundred, a help page that no longer fit on my terminal screen, and a build pipeline that broke because two of those new flags silently conflicted. The tool wasn’t better—it was heavier, noisier, and less predictable. Yet somewhere in a product roadmap, someone had decided that adding more features was the same as adding more value.

I’m Kai Lindström, and I’ve spent the better part of my career watching developer tools chase a feature-count arms race they can never win. The pattern is so consistent it’s almost a law: a lean, focused tool solves one problem beautifully. It gains traction. Then the requests pour in. “Can it also handle YAML?” “What about a GUI?” “Why doesn’t it integrate with Jira?” The maintainers, often well-intentioned, start bolting things on. Three years later, the tool does everything poorly and nothing memorably. This isn’t just a minor annoyance—it’s a fundamental misunderstanding of what makes a developer tool effective.

Feature Density vs. Cognitive Load: A Trade-off Nobody Talks About

Every feature you add to a tool increases its surface area. That’s obvious. Less obvious is the nonlinear relationship between feature count and the cognitive load imposed on the user. A developer doesn’t just need to know that a feature exists; they need to understand its boundaries, its interactions with other features, its edge cases, and its failure modes. When a tool has ten features, the mental model is manageable. At fifty features, you’re no longer using the tool—you’re negotiating with it.

Take a real-world example: curl. It’s not a simple tool by any stretch—its man page is legendary for its length—but its surface area is disciplined. Every flag maps to a clear protocol-level concern. You don’t find curl --integrate-with-slack or curl --auto-retry-on-caffeine-depletion. The maintainers have resisted the temptation to turn it into a Swiss Army knife that also slices bread. Contrast that with a certain cloud CLI I won’t name, where a single command can take eighteen positional arguments, some of which change meaning depending on the value of a flag three arguments earlier. That tool has more features. It is not, by any sane measure, a better tool.

Close-up of a developer's hands typing code on a mechanical keyboard, lit by a dim monitor glow

The Configuration Swamp: When Flexibility Becomes a Liability

One of the clearest symptoms of feature bloat is configuration sprawl. It starts innocently. A tool has a single config file with a handful of top-level keys. Then someone wants per-environment overrides. Then someone wants conditional logic. Then someone wants to reference environment variables inside string values. Before long, you’re debugging a YAML file that has its own import system, its own templating language, and a hidden dependency on the phase of the moon.

I’ve seen teams spend more time arguing about config structure than writing actual application code. The tool’s flexibility becomes a trap: because you can express any deployment topology, you now must express it, and you’re responsible when the twelve-layer inheritance chain produces a value nobody expected. A better tool would have said “no” to half those features and shipped a design that makes the common case obvious and the uncommon case impossible. That’s not a lack of ambition; it’s respect for the user’s attention.

Consider ESLint. It’s configurable to a fault—you can write custom rules, custom parsers, custom formatters. But that configurability comes with a maintenance cost. Every new rule is a potential false positive. Every override in an .eslintrc file is a silent vote of no confidence in the defaults. A tool that shipped with a smaller, more opinionated rule set and a harder boundary around customization would create fewer bikeshedding conversations and more consistent codebases. Instead, we have an ecosystem where every team builds their own bespoke linting universe, and nobody’s is quite compatible with anyone else’s.

The Performance Tax Nobody Budgets For

More features don’t just clutter the interface—they slow things down. I’m not talking about the obvious cases where a tool becomes bloated because it’s loading seventeen plugins at startup. I’m talking about the subtle, pervasive tax that optional features impose on every code path. A build tool that supports incremental compilation, file watching, remote caching, and distributed execution has to check for all of those conditions even when you’re running a cold build on a single machine. The branches are still there. The abstractions are still there. The initialization cost doesn’t disappear just because you didn’t pass --distributed.

I once profiled a popular JavaScript bundler and found that over thirty percent of its startup time was spent resolving configuration options that ninety-five percent of users never touched. That’s thirty percent of every developer’s iteration loop spent servicing features they didn’t ask for. The bundler’s maintainers weren’t lazy—they were responsive. They’d added every feature the community requested. The result was a tool that pleased everyone in surveys and frustrated everyone in daily use.

A developer staring at a screen filled with complex terminal output and configuration files

Why We Keep Falling for It: The Psychology of Feature Checklists

If feature bloat is so obviously harmful, why does it keep happening? Part of the answer is that features are easy to compare. When you’re evaluating two tools, a checklist feels objective. Tool A has a built-in diff viewer. Tool B doesn’t. Score one for Tool A. But what that checklist doesn’t capture is that Tool B’s diff viewer, when it exists as an external command piped through standard Unix tools, composes better with the rest of your workflow. Tool A’s built-in viewer might be slightly more convenient in isolation, but it locks you into its UI, its keybindings, its assumptions about how you want to view changes.

This is the hidden cost of integrated features: they break composability. The Unix philosophy—small tools that do one thing well and communicate via text streams—isn’t just an aesthetic preference. It’s an architectural principle that maximizes the value of each component by making it reusable in contexts the original author never imagined. Every time a tool absorbs functionality that could have been a separate program, it shrinks the design space for everyone downstream.

We also mistake activity for progress. A changelog with fifty bullet points feels more impressive than one that says “removed three underused features and simplified the configuration format.” But the latter is often the braver, more valuable release. It takes confidence to subtract. It takes a deep understanding of your users to know which features are actually carrying their weight and which are just inflating the download size.

Case Study: Git’s Porcelain vs. Plumbing

Git is a fascinating example because it gets both sides of this argument right and wrong at the same time. The Git we use day-to-day—commit, branch, merge—is the porcelain, a user-facing layer that has accumulated a staggering number of flags and subcommands over the years. git log alone has so many formatting options that entire books have been written about them. That’s feature bloat in its classic form.

But underneath the porcelain is the plumbing: git hash-object, git update-ref, git cat-file. These are small, focused commands that operate directly on Git’s object store. They haven’t changed much in years. They don’t need to. They compose beautifully, and they’re the reason tools like Magit and lazygit can exist—they provide a stable, minimal interface that higher-level tools can build on. The lesson isn’t that Git is perfect; it’s that the valuable part of Git is the part that resisted feature creep. The porcelain is what we complain about. The plumbing is what we depend on.

A whiteboard covered in hand-drawn diagrams of Git workflows and branching strategies

What a Better Approach Looks Like

So what would a developer tool built on the principle of “less is more” actually look like? First, it would have a small, stable core that solves one problem completely. Not ninety percent of the problem—one hundred percent, for a carefully defined scope. If it’s a test runner, it runs tests. It doesn’t also manage fixtures, spin up databases, or format output in seventeen different styles. It runs tests, and it exposes a clean interface that lets other tools handle the rest.

Second, it would invest heavily in defaults. The best tools feel boring. They don’t ask you to make decisions before you’ve written a single line of code. They come with a configuration that works for eighty percent of use cases, and they make it slightly awkward to leave that happy path—not impossible, but deliberate. When you override a default, the tool should make it clear that you’re taking on maintenance responsibility for that choice.

Third, it would prioritize composability over integration. Instead of building a plugin system, it would produce structured output that other programs can consume. Instead of adding a built-in linter, it would make it trivial to pipe source through an external one. This isn’t just about keeping the codebase small—it’s about respecting the Unix ecosystem and acknowledging that no single tool can anticipate every workflow.

A Concrete Example in Code

Let’s make this tangible. Imagine a hypothetical build tool called forge. The bloated version might look like this:

# forge.yaml - the bloated version
build:
  steps:
    - type: lint
      linter: eslint
      config: .eslintrc.custom.js
      auto_fix: true
      ignore_patterns:
        - "**/vendor/**"
    - type: compile
      compiler: tsc
      tsconfig: tsconfig.build.json
      incremental: true
      output_dir: dist
    - type: test
      runner: jest
      coverage: true
      threshold: 80
  watcher:
    enabled: true
    debounce_ms: 200
    ignore_dotfiles: true
  notifier:
    on_failure: slack
    slack_webhook: ${SLACK_WEBHOOK_URL}

Now look at what a focused version might require:

# forge.yaml - the focused version
source: src
output: dist

The focused version assumes you’ll lint with your own linting command, compile with the compiler your project already uses, and run tests with whatever test runner you’ve configured. It doesn’t try to own your entire pipeline. It does one thing—build—and it gets out of the way. The first version looks more powerful on a feature comparison page. The second version is what I’d actually want to maintain over the lifetime of a project.

When Features Are Actually Worth Adding

I’m not arguing for ascetic minimalism. Features exist because they solve real problems. The question is whether a given feature solves a problem that belongs inside the tool or whether it’s papering over a problem that should be solved elsewhere. A good heuristic: if adding the feature would require the tool to understand a new domain—networking, UI rendering, notification delivery—it probably doesn’t belong. If the feature deepens the tool’s ability to do its core job—making a compiler produce better error messages, giving a debugger access to more runtime information—it’s likely worth the cost.

Another test: can the feature be implemented as a separate tool that composes with the original? If the answer is yes, do that first. If nobody uses the separate tool, the feature wasn’t as important as the requester claimed. If everyone uses it, you’ve validated the need without polluting the core codebase, and you can consider a tighter integration later—carefully, and with the understanding that integration is a one-way door.

The Long-Term Cost of Saying Yes

Every feature is a promise. When you add a flag, you’re promising that flag will continue to work in future versions. When you add a configuration format, you’re promising to support it, document it, and debug it when users inevitably combine it with other features in ways you never tested. Those promises compound. A tool with five features has five promises to keep. A tool with a hundred features has a hundred promises, and the odds that at least one of them will break in any given release approach certainty.

I’ve seen maintainers burn out under the weight of their own changelogs. They weren’t bad programmers. They were generous ones, and their generosity was exploited by a user base that treated “can you add…” as a cost-free request. It’s never cost-free. The cost is paid in complexity, in maintenance burden, in onboarding friction, and ultimately in the trust users place in the tool’s stability.

The tools I reach for today are the ones whose maintainers have learned to say no. They ship less, and what they ship lasts longer. Their documentation is shorter and actually accurate. Their behavior is predictable because there’s less of it to predict. They don’t win feature comparison checklists, and they don’t need to. They win where it matters: in the daily experience of the developers who depend on them.

Frequently Asked Questions

How do I know if a developer tool has too many features?

Look at the documentation. If the table of contents alone takes more than a minute to scan, the tool is probably doing too much. Another sign: you find yourself searching Stack Overflow for the “right way” to do something that should be obvious. If a build tool has seventeen different ways to specify dependencies, the problem isn’t your understanding—it’s the tool’s design. A healthy tool makes the simple case effortless and the complex case possible, not the other way around.

Isn’t it better to have a feature and not need it than to need it and not have it?

Not in developer tools. Features you don’t use still impose costs: they increase the attack surface for bugs, they make configuration more error-prone, and they clutter the namespace you have to think through when debugging. In a physical toolbox, an extra wrench doesn’t hurt anything. In a software tool, every extra feature is a wrench that might fall into the gears. The leaner tool is often the more reliable one, even if it can’t do every niche task out of the box.

How can I avoid building bloated tools when users keep asking for more features?

Invest in extension points, not built-in features. A well-designed plugin API or a commitment to structured output lets power users solve their own problems without dragging the entire user base along. Also, practice saying no with data. When someone requests a feature, ask them to show you the workflow that’s currently painful. Often you’ll find that the real problem is a missing composition primitive, not a missing feature. Fix the primitive, and you’ll unlock ten use cases instead of just one.

The next time you’re evaluating a developer tool, try this: ignore the feature list entirely. Instead, ask how long it takes to go from zero to a working, repeatable result. Ask what the tool doesn’t do, and whether that omission is deliberate. Ask whether the maintainers have ever deprecated a feature, and if so, how they handled it. The answers will tell you more about the tool’s quality than any number of checkmarks ever could.