Skip to content

Software Engineering

Building software is the easy part. Building it with other people is the job. Notes from the trade.

Begin a Brag Doc

Document the narrative before someone else drafts it.

NOTE πŸ“œ This is a post about Software Engineering. All views are my own and do not represent my employer. Please review my Disclosures.

πŸ“„ Brag Doc Template The structure below, as copy-paste-ready markdown.

πŸ“‹ My Brag Doc The personal version: NBA arenas, gym PRs, concerts.

When I entered tech, I assumed my manager knew everything I was working on. I gave status at weekly standups. We went deep in our one-on-ones. I figured he was keeping track.

He was, but humans don't work that way. A manager runs a 6-12 person team. They hold the big picture: the major milestones, the big PRs, the design docs that landed. The rest fades fast, and compacts with every new cycle.

This is why you should keep a brag doc. A brag doc is a running log of your impact: wins, artifacts, metrics, growth. You own it. You maintain it. It's ready when you need it.

There are plenty of other reasons have one:

  1. Memory is unreliable. Both yours and your manager's. By the time calibration happens, Q1 work has been buried under three quarters of newer fires.
  2. Promotion packets need scaffolding. You can't synthesize 12 months of work into a 500-word write-up the night before. You need raw material.
  3. Calibration is comparative. Your skip-level is in a room comparing you against four other engineers. Specificity wins.
  4. Job changes happen. The doc you keep for promotion is the same doc you grab when a recruiter pings you about a senior role.
  5. Leadership rotation. Your manager rotates. So does your skip, your director, your VP. Each new chain inherits a thin model of your work, and only you can fill it in.

Brag Evolution

Initially, I communicated my achievements once a performance cycle: through the write-up. You know the one. List five achievements from the last 12 months. Or list the projects you worked on and explain their outcomes. My manager would read it and form a judgement. This was fine, for a while.

It's hard to craft a narrative in the format of the performance review. You have to explain the high level and go into gritty details, all inside a word limit. By the time you've explained the project, you have 100 words left for the impact. By the time you've made the impact case, the section is over. You cherry-pick the wins you can summarize in two sentences, not the wins that mattered most.

Around every performance review, my manager was surprised by all I had accomplished. This is actually not a good sign. It meant my manager had a thin view of what I was doing for most of the year. I needed to change that.

Then came time for my first promotion. I had to shape two years of work into a coherent case. I pored over old performance reviews, gathered everything into one doc, and updated it for the newest wins. It was a lot of work. Hence this meme:

Late-night scramble to assemble a perf write-up

The system was unsustainable.

Then came the brag doc. Thirty minutes the last Friday of each month. A few bullets, minimum. When the next perf review came around, I copied the relevant rows into the form. When the next promotion came around, the doc was already most of the packet. The mad scramble never came back.

Philosophy

Over the years of maintaining a brag doc, I have gotten quite opinionated on them. I thought they should:

Know your audience. This document isn't for you, it's for your management chain. Imagine your manager in a review session and they need to quickly justify your contributions: you need structure and hierarchy. Or you're a senior leader reviewing this document to determine promotion: you need a story and a vision. Write your document for them, not you.

Focus on impact, not contributions. I've tried a lot this cycle, some things landed resoundingly, some things landed poorly. You don't need to omit your missteps, because this proves your work was challenging. Instead, you need to focus on your most impactful work and how you got there.

Write as you go. A doc you compile two weeks before review is fragmentary at best. The doc you maintain weekly stays grounded in real artifacts: PRs you actually shipped, meetings you actually ran, bugs you actually closed.

Quantify ruthlessly. "Improved performance" is forgettable. "Cut p99 latency from 280ms to 95ms across a rollout to 1M users" is not.

Tie work to scope. Every entry should answer: what changed, who benefited, and at what scope. Without scope, "fixed a bug" reads identical to "led a multi-team migration."

And, because we live in the AI era, one last principle:

Make it AI friendly. Maintaining the doc is grunt work. Point an AI at your raw materials and it writes most of this for you. You handle the craft, curation, and story. The AI handles the toil.

Doc Template

Owning the doc lets you control the narrative: what gets surfaced, how, and to whom. The alternative is leaving it to memory.

Goals

You've probably already set these with your manager. If you're already tracking goals elsewhere, paste them in verbatim. If not, write down what you and your manager agreed to in your last planning conversation.

Examples:

  • Lead the migration of the user-auth subsystem from Service A to Service B by Q3.
  • Onboard and ramp two new mid-level engineers to independent contribution.
  • Reduce p99 latency on the recommendation API to under 100ms.

TL;DR

Bullet points of the big landings throughout the year. Punchy, data-driven, memorable. Reads like a cross between a stats sheet and a press kit.

What metrics back up your goals? Did you conduct interviews? Host an intern? Cover a broad set of data points and go deep on a few.

Examples:

  • Shipped the auth migration on the Q3 date, zero customer-facing regressions.
  • Cut p99 from 280ms to 91ms; backed by load-test results and a week of post-rollout monitoring.
  • Authored 4 design docs, reviewed 31, mentored 2 new engineers to independent contribution.
  • Closed 47 bugs, including 3 sev-2 production incidents.

Executive Summary

The overview of your cycle, written for skim-readers. Picture an outsider or a senior leader with a few paragraphs of attention: what do they need to know?

Example:

This year I focused on three workstreams: the auth migration, latency reduction, and team growth. The auth migration shipped on time and unblocked the broader platform refactor. Latency work brought us within SLO for the first time in 18 months. I also onboarded two new engineers, and now formally mentor one of them.

Workstreams

This is the meat. For each major workstream you contributed to, write 2-4 paragraphs covering: the problem, the approach you took, the outcome, and your specific contribution. Be honest about scope. If you were one of five engineers, say so. If you led the design and three others implemented, say that.

I usually have 3-5 core workstreams per cycle. Anything fewer reads as low impact. Anything more reads as scattered.

Example:

Auth Migration. Service A had been our auth provider since 2019, but its rate-limiting story didn't scale to our new mobile clients. I led the design, partnered with the security team for review, and shipped the migration to 40% of traffic in Q2 and 100% in Q3. Net result: 12x rate-limit headroom and a 30% reduction in auth-flow complexity for downstream services.

Artifacts

Now that you laid out your case, you need data to support it. You proved the quality of your work; now it's time to prove the quantity.

Here, I post everything:

  • Documents especially design docs
  • Pull Requests not just core work, but rollbacks, bug fixes (even if the bug is in your code), any sort of code contributions
  • Tickets you've closed with highly-visible ones (i.e., high number of comments) at the top
  • Meetings such as alignment sessions, peer programming, or presentations you've done
  • Awards from your peers or your management chain

The point isn't to pad the section. Calibration discussions often pivot on a specific artifact you forgot to mention. Better to have it listed and unused than missing and decisive.

Peer Reviewers

Who did you work with? In what domains? What were their contributions? Your manager will ask all three when reading your writeup. The list isn't only so they can ask others about your work; it also helps level you. If you're level N working with N+1 and N+2 reviewers who vouch for you, that's a strong promotion signal.

This list, along with your brag doc, is one of the few levers you have during this process. So make it solid. Talk with your reviewers before the cycle; ask if they have feedback for you.

Growth

The section reviewers trust most. A self-aware gap analysis signals seniority better than any single win. Name real things; vague "I should communicate more" reads as filler.

Three sub-sections cover it:

  • What you got better at. Specific skills, with evidence.
  • What you missed or got wrong. Concrete misses, with what you'd do differently.
  • Where you want feedback. Direct questions your reviewers can answer.

Example:

What I missed. Q3 incident cluster: I under-communicated to skip-level for ~2 weeks. Course-corrected with a weekly written status; feedback positive in Q4.

Quarterly Log

The raw material that feeds everything above. One entry per quarter, accumulated monthly. Each entry is a structured set of bullets: shipped, designed, helped, incidents, decisions, quotes, lessons, metrics.

This section is the most important and the most unglamorous. The Workstreams, TL;DR, and Executive Summary all draw from it. If you maintain nothing else, maintain this. By the end of the year, you'll have a year of evidence ready to be cherry-picked.

Example slice from a Q4 entry:

- Shipped:   RFC-217 reached 100% rollout
- Decisions: Deferred Project Zeta to H1 (~6 eng-weeks)
- Metrics:   p99 checkout 312ms β†’ 297ms (post-canary)

Your Own Brag Doc

This inspired me to publish my own brag doc, of sorts. I took the things I'd done outside of work: every NBA arena I've visited, every concert I attended, every gym PR. Same idea as the work brag doc, just for the parts of life that don't have a perf review.

You can read mine here.

Whichever version you keep, personal or work or both, start it today. Open a doc, paste in three things you did this week, and don't stop. The version you'll have in 6 months is the one you'll wish you had today. Write it once a month, cash it once a year.

Brag Doc Template

A simple template for tracking your engineering impact across a performance cycle.

Copy this file, fill in your own content. Update the Quarterly logs once a month. Promote items into the upper sections every 2-4 weeks. At review time, you trim and reorder. Don't write from scratch.


Metadata

Field Value
Owner Your name
Level e.g., Senior SWE (L5)
Period e.g., H2 2025
Manager Name
Last updated YYYY-MM-DD

Goals

The 2-5 goals you set with your manager at the start of the cycle. Mirror them at the end with a status and one-line outcome.

Goal Status Outcome
Lead the migration of Service X to Service Y by Q3 🟒 Done Migrated 12 services; cut p99 latency 30%
Mentor 2 junior engineers to mid-level readiness 🟑 Partial 1 promoted, 1 in progress

🟒 done Β· 🟑 partial Β· πŸ”΄ missed Β· βšͺ️ deprioritized


TL;DR

3-7 bullets, each one quantified. Lead with the biggest thing. This is what your manager pastes into calibration.

  • Led launch of, cutting checkout latency 38% (p99 480ms β†’ 297ms) for 12M MAU.
  • Authored the Service Mesh RFC adopted by 4 orgs (~140 engineers), eliminating ~3 FTE-quarters of duplicated work.
  • Designed and shipped the new rate-limiting kernel; cut error budget burn 67% YoY.
  • Mentored 3 ICs; 2 promoted (L3β†’L4, L4β†’L5).
  • Onsite interviewer for 28 candidates; closed 4 hires.

Executive Summary

One to three short paragraphs. What was the team trying to do, what was your role in it, what's different because you were there.

In H2 2025 the Checkout org's primary goal was to launch in 14 new markets. I owned the payments-integration workstream end-to-end, taking it from an ambiguous "support local payment methods in EU and APAC" charter to a shipped system processing ~$180M GMV/month at p99 < 300ms. The work spanned four teams and required a new abstraction layer (RFC-217), since adopted by Subscriptions and Marketplace.

Beyond the project, I sponsored two mid-level engineers through their first cross-team designs; both now lead their own workstreams. Growth area: I under-invested in upward communication during the Q3 incident cluster. I've started a weekly written status to my skip in Q4 and feedback has been positive.

Workstreams

One subsection per major project. Use STAR: Situation, Task, Action, Result. 3-5 per cycle is typical. Fewer reads as low impact. More reads as scattered.

[Outcome-shaped title, e.g., "Made international expansion possible"]

  • Situation. What problem existed before you started? Quantify the pain.
  • Task. What you were asked, or what you decided, to do.
  • My role. Sole engineer / TL with 4 ICs / co-lead with @alice / reviewer & sponsor of @bob's work. Honest attribution makes your real contribution legible.
  • Action. 3-6 things you did. Decisions you made and why. Trade-offs you owned. Partners (PM, design, data) you worked with.
  • Result. Numbers. Adoption. Behavior change.
  • Artifacts. Links to design docs, PRs, launches, dashboards, postmortems.

Repeat the block above for each major workstream.


Artifacts

Direct links. Reviewers cross-reference these. Keep it skim-able.

  • Design docs / RFCs. Title: one line on impact
  • Significant PRs. Title: one line on impact
  • Talks / posts. Title: venue, audience size
  • Incidents / on-call. e.g., IC for 3 sev-2s; 8 shifts; ~24 pages
  • Hiring. Loops: 28 Β· Hires closed: 4 Β· Bar-raiser: 12
  • Above and beyond. Open-source, internal community work, DEI, conference talks

Peer Reviewers

Who can speak to which work. Share this section with them so they know what to comment on.

Reviewer Relationship What they can speak to
@alice Cross-team peer RFC-217 review, payments collab
@bob Direct mentee Mentorship effectiveness
@carol Sister-team TL Cross-team alignment, incident response

Growth

The section reviewers trust most. Name real things. "I should communicate more" is not a growth area.

What I got better at. e.g., Writing strategy docs that get traction outside my org. 0 β†’ 3 adopted RFCs.

What I missed or got wrong. e.g., Q3 incident cluster: I under-communicated to skip-level for ~2 weeks. Course-corrected with a weekly written status; feedback positive in Q4.

Where I want feedback. e.g., Am I operating at the L6 bar on technical scope? Where am I still acting like a Senior IC instead of a Staff IC?


Quarterly Log

The raw material. Update once a month; one entry per quarter. Promote items upward every 2-4 weeks.

Template entry

## YYYY Qn

- Shipped:   [Major PRs, launches, decisions this quarter]
- Designed:  [RFCs, docs, proposals]
- Helped:    [Mentees, unblocks, sponsored work]
- Incidents: [On-call shifts, sev levels, what I learned]
- Decisions: [Strategic decisions that landed]
- Quotes:    [Positive feedback received this quarter]
- Lessons:   [Honest notes; private to you]
- Metrics:   [Dashboard movement traceable to me]

Example entry

## 2025 Q4

- Shipped:   RFC-217 (payments abstraction) reached 100% rollout
             Rate-limit kernel to 100% (canary started Q3)
             Onboarded @marketplace onto the new auth flow
- Designed:  RFC-217 v3 published; reviews from 4 orgs
             RFC-218 (Service Mesh follow-on) draft circulated
- Helped:    Co-authored @alice's L3 β†’ L4 promo packet
             Sponsored @bob's first cross-team design review
             Unblocked @marketplace on auth migration (3 Γ— 40min)
- Incidents: IC for 2 sev-2s (Oct 14, Nov 2); 12 pages total
             Cut false-positive rate 60% via alert config
- Decisions: Deferred Project Zeta to H1 (~6 eng-weeks saved)
             Pushed back on premature dual-write; saved a quarter
- Quotes:    @director on RFC-217: "clearest doc this quarter"
             @skip on RFC-218: "exactly the right altitude"
- Lessons:   4-day meeting series that should've been a doc;
             next time, propose async first
- Metrics:   p99 checkout 312ms β†’ 297ms (post-canary)
             Build time 41min β†’ 17min (CI parallelization)
             Error budget burn 4.2Γ— SLO β†’ 1.4Γ— SLO YoY

The Vanishing Keystrokes Bug

How a faceless background agent ate my typing, and the script that caught it.

A few days into an uptime, deep in something, my Mac would stop listening. Not a crash, not a freeze. The cursor still moved, windows still highlighted under it, I could click anything. But the keyboard went dead: I'd type a whole sentence into a window that looked focused and watch zero characters land. Clicking didn't help. Sometimes it cleared after a few seconds; sometimes I rebooted and bought another day or two of quiet.

A bug that only shows up after hours of uptime, never on a cold boot, and clears on reboot is the worst kind. You can't reproduce it on demand, so you can't poke at it. I finally cornered it, and the culprit wasn't what I assumed.

The Symptoms

If your Mac does this too, start here. The shape of the failure tells you where to look.

  • The mouse works. The keyboard doesn't. Pointer moves, clicks land, but no window accepts text.
  • It's system-wide, not one app. Every window is dead, whichever one you click into.
  • It builds up over uptime. Fine on a fresh boot, more frequent the longer the machine stays awake.
  • A reboot fixes it. Temporarily. It always comes back.
  • The focused window looks slightly de-focused, title bar greyed out, as if nothing is frontmost.

That combination rules a lot out. It isn't a hardware keyboard fault: the mouse and keyboard share enough of the input stack that a real HID failure takes both. It isn't one misbehaving app. It points at the part of the OS that decides where keystrokes go, and at something that corrupts that decision the longer you stay logged in.

Two Kinds of "Front"

macOS tracks "in front" in two places that are supposed to agree.

  1. The active application (LaunchServices and NSWorkspace). Apple defines frontmostApplication bluntly: the app that receives key events. lsappinfo reports it.
  2. The key window (WindowServer). Within the active app, keys flow to the key window, then to its first responder, the text field your cursor sits in. No window that can become key means no key window, and nowhere for text to land.

A third notion, Accessibility's AXFrontmost (what AppleScript's System Events reports), tracks the app whose UI is actually up front.

On a healthy Mac all three agree and you never think about it. The bug lives in the gap: if LaunchServices says one app is active while Accessibility says another, the OS routes your keystrokes to an app you aren't looking at. If that app has no window, they evaporate.

Bug Catcher

Check both notions from the terminal, especially while the bug is happening:

# LaunchServices: who owns key events?
lsappinfo info -only name "$(lsappinfo front)"

# Accessibility: who's actually up front?
osascript <<'EOF'
tell application "System Events"
  name of first application process whose frontmost is true
end tell
EOF

Healthy, these match. Mine didn't:

LaunchServices : "Logitech G HUB Agent"
Accessibility  : wezterm-gui

LaunchServices thought a faceless agent, the Logitech G HUB Agent, was the active app receiving key events, while the app I was clicking into was my terminal. My keystrokes were routing to an agent with no window.

Two commands catch it if your timing is lucky. The bug is intermittent, so I wrote a monitor that shouts the moment the two disagree.

focus-spy

It polls both notions once a second, logs every change, and flags MISMATCH when the PIDs differ. Drop it in your $PATH, chmod +x it, run it.

#!/usr/bin/env bash
# focus-spy - find out what's stealing keyboard focus on macOS.
#
# macOS tracks "the frontmost app" in two places that should agree:
#   * LaunchServices / NSWorkspace - the app that RECEIVES KEY EVENTS
#       (what `lsappinfo front` reports)
#   * Accessibility (AXFrontmost)  - the app whose UI is up front
#       (what System Events reports)
# When a background agent shoves itself into the first one without a
# real window, the two disagree, and your keystrokes fall in the gap.
# This logs both once a second and shouts when their PIDs differ.
#
# Usage:
#   focus-spy             # watch (Ctrl-C to stop)
#   focus-spy mark NOTE   # mark the instant typing dies
#   focus-spy report      # print the timeline, sorted

set -uo pipefail
LOG="${FOCUS_SPY_LOG:-$HOME/.local/state/focus-spy.log}"
mkdir -p "$(dirname "$LOG")"

ts() { date '+%Y-%m-%d %H:%M:%S'; }

# LaunchServices' frontmost (the key-event owner) as "name#pid".
# Compare by PID, not name: the two APIs spell the same app
# differently ("WezTerm" vs "wezterm-gui"); only the PID is identity.
ls_front() {
  local asn name pid
  asn="$(lsappinfo front 2>/dev/null)"
  name="$(lsappinfo info -only name "$asn" 2>/dev/null)"
  name="${name#*=}"; name="${name//\"/}"
  pid="$(lsappinfo info -only pid "$asn" 2>/dev/null)"
  pid="${pid##*=}"
  printf '%s\n' "${name:-?}#${pid:-?}"
}

# Accessibility's frontmost (the visible app) as "name#pid". First run
# may prompt your terminal for Automation access to System Events.
ax_front() {
  osascript 2>/dev/null <<'OSA'
tell application "System Events"
  set p to first application process whose frontmost is true
  return (name of p) & "#" & (unix id of p)
end tell
OSA
}

watch() {
  echo "focus-spy: watching (Ctrl-C to stop). Log: $LOG"
  local prev="" ls ax line tag
  while :; do
    ls="$(ls_front)"; ax="$(ax_front)"
    line="ls=[$ls]  ax=[$ax]"
    if [[ "$line" != "$prev" ]]; then
      [[ "${ls##*#}" == "${ax##*#}" ]] && tag="ok      " || tag="MISMATCH"
      printf '%s\n' "$(ts)  $tag  $line" | tee -a "$LOG"
      prev="$line"
    fi
    sleep 1
  done
}

case "${1:-watch}" in
  watch)  watch ;;
  mark)   shift
          printf '%s\n' "$(ts)  MARK      >>> ${*:-typing died} <<<" \
            | tee -a "$LOG" ;;
  report) sort "$LOG" 2>/dev/null || echo "no log yet" ;;
  *)      echo "usage: focus-spy [watch|mark NOTE|report]"; exit 1 ;;
esac

Leave focus-spy watch running in a spare terminal. The instant the keyboard dies, run focus-spy mark "typing died" in any shell, then focus-spy report. Look for a MISMATCH line: whatever sits on the ls= side is your thief. Every one of mine read ls=[Logitech G HUB Agent].

Terminal status flipping from ok to MISMATCH and back as a faceless agent seizes the keyboard

The two fronts diverging on demand. A faceless stand-in agent (built to reproduce the bug) seizes the LaunchServices slot while the terminal keeps Accessibility focus, so the status flips to MISMATCH, then back the instant it exits.

The Smoking Gun

A monitor tells you who; a kill test tells you whether you're right. The LaunchServices front process read Logitech G HUB Agent more than twenty samples in a row. So I killed the stack and watched it the instant it died:

killall lghub_agent lghub_system_tray lghub
lsappinfo info -only name "$(lsappinfo front)"

The active app snapped back to WezTerm and held. Both notions agreed. Typing was solid. G HUB's agent had been parking itself in the active-app slot and never letting go.

Why It Breaks

The active app receives key events, routed to its key window's first responder. An agent app (LSUIElement, no Dock icon) can still become the active app, and the old SetFrontProcess API was deprecated for NSRunningApplication.activate, which developers have long reported is easy to leave in an inconsistent state. G HUB falls into exactly that: it activates itself, probably an unbalanced activate on a device-poll loop, and becomes the active app without a window that can become key. The keyboard now points at a process with no first responder, so keystrokes are dropped, not stolen, just discarded, until you click into a real app. The mouse still works because mouse events are hit-tested by pointer location, not by which app is active. (Don't confuse it with Secure Input, where an app legitimately swallows every keystroke and forgets to stop; there both notions of "front" still agree. The mismatch is the tell.)

You're not imagining it, either. Another engineer built the same kind of monitor and clocked G Hub grabbing focus 47 times in four minutes. A MacRumors thread nails it: G Hub "keeps trying to get focus ... since it only runs in the background and has no window to receive the focus, the frontmost app of the system loses focus." Logi Options+ does the same. The trigger looks like wake and device re-enumeration, which fits why it snowballs over uptime and resets on reboot. The lesson: when you finally name a weird bug, search the name. You're rarely the first to hit a real defect.

The Fix

Right now, quit it:

killall lghub_agent lghub_system_tray lghub

Permanently: a launch agent restarts it at login (/Library/LaunchAgents/com.logi.ghub.plist, RunAtLoad). Move it aside, reversible:

sudo mv /Library/LaunchAgents/com.logi.ghub.plist \
  ~/Documents/backup/com.logi.ghub.plist

Then check System Settings, General, Login Items & Extensions for any Logitech entry. If you don't use Logitech G gaming gear, uninstall G HUB outright; Logi Options+ already covers a normal mouse.

My Mac types again. Now when focus feels off, I reach for focus-spy watch in a corner terminal, because the next thief trips the same wire.

AI Won't Replace Software Engineering (Yet)

But it will raise the bar.

NOTE πŸ“œ This is a post about AI. All views are my own and do not represent my employer. Please review my Disclosures.

I have a boring opinion: AI is a tool.

Like a text editor, a compiler, grep. And like every tool, it's clueless, easily nudged off track, and sometimes too eager to please. That's not a flaw of character. It's a stochastic model, not a person.

It is a groundbreaking tool. My third most-used after the text editor I'm typing this in and the shell running it. No tool has rooted itself this deep, this fast. Long-abandoned projects look polished. Ideas I shelved as too tedious are a prompt away. It's almost magic.

The numbers feel magic too:

The headlines are magic too:

Still, I'd taper expectations. The underlying tech is human language, not a model of the world. Language is a symptom of intelligence, not the cause. AI is still a tool.

A wooden tray of tools β€” Vim manual, compiler stamp, grep magnifying glass, slide rule β€” and a glowing crystal labeled AI, with a hand reaching for it.

Replacing Software

I've been coding with AI assistants for over two years. The shortest summary I have:

The best intern I could ask for. It listens, chips away, makes some bad assumptions. On familiar ground, it shines. Off-trail, the output is off-trail too. With clear direction, it solves most of what a working engineer hits in a day.

I've watched it fall flat in narrow places (LaTeX Tikz, custom diagrams) and broad ones (overweighting the current file instead of reaching for world knowledge). It's not just me:

My workflow is unrecognizable from two years ago. The objective hasn't moved: ship software that scales. Code is one piece. AI helps with most of them, but someone still has to orchestrate top-to-bottom. AI turns a rowboat into a crewed ship, and the rower into the captain. They route, monitor, and adjust. I find AI roughly useless without me at the wheel, nudging it back on course.

A pilot in a cockpit watching robot crew assemble a half-built airliner mid-flight at sunset.

This may age badly, but I'll stake it: the software engineering industry keeps growing for a while yet. It contracts later, when models meet or beat human intelligence and the captain stops being load-bearing. For now we're building the ship and flying it at the same time.

So if we aren't being replaced, what are we doing?

Raising The Bar

When the compiler arrived, demand for engineers went up. More ambitious projects became feasible; the harder problems got reached for, not skipped. Productivity tools tend to do that β€” they raise the ceiling before they shrink the floor.

The lesson: lean into where AI is strong. Onboarding, legacy code, the routine. I haven't written an argparse CLI by hand in over a year, and I don't miss it. Side projects that would have taken months take days. The cobwebs in my codebases are fun to clear, not tedious. Professionally, however, it's a different story.

The difference is simple: I read every line of code I ship at work. I don't read every line I ship at home. Side-project stakes are low. if it breaks, someone files an issue or sends a patch. Production stakes aren't.

When models pass human intelligence, I'll stop reading the code to fix it and start reading the code to learn from it. It'll know more than I do. Until then, intelligence is bounded by context windows and language. I'm not staking a career on a pile of tokens and linear algebra β€” neither by skipping the code review, nor by quitting the field.

A few real obstacles on the way there:

Those are problems for us, as humans, to fix.

As engineers, we do what we did when the compiler shipped, and the IDE, and the package manager: we build more. Better. Sometimes the bottleneck is human review; sometimes it's reaching past where the model can see. The bar moves up.

Every engineer now runs a small team of capable agents. We can build anything we can specify. We can serve very small audiences with very specific software, or very large ones with very hard software. The work isn't going away. It's getting harder, and more interesting, at the same time.

Still The Captain

I trust the model on the easy stretches. I read every line on the hard ones. That doesn't change until the ship doesn't need a captain. And from where I'm standing, mid-flight, it still does.

There's No Place Like $HOME

A ten-year journey of mostly Vim, then VS Code, then back to Vim.

NOTE πŸ“œ This is a post about AI. All views are my own and do not represent my employer. Please review my Disclosures.

Let me take you through ten years of text editors.

I've been programming primarily via a shell since 2016, and my dotfiles go back to 2017. I wrote the thesis that you should care deeply about the tools you use. In 2026 I use the same tools I did then. But a lot else has changed.

I entered big tech, and noticed how few engineers around me shared my tool suite. Most lived in a GUI editor where the terminal was optional and a shell was something you bolted on. I found it increasingly hard to communicate intent with colleagues. So, out of FOMO, I decided to try something different. After a decade of Vim, I changed my daily driver to VS Code.

Then, around 2025, the work itself changed. My workload and my approach to it were suddenly different.

IDE For Me?

At first it was pretty nice. The tooling was integrated: version control was a panel instead of a man page, the file tree sat right there, a debugger and a run button one click away. Nothing needed a name before I could use it; every action was a menu, every menu a button. For someone starting out it lowers the floor, and you can be productive before you understand the machine underneath.

My "Oh, this just works" moment was subtle but delightful: my compiler would print a filepath, line, and column in its output, and one day I realized I could click it and land on that exact spot in the source. It was exactly what IDEs promise: it just worked.

I took many other lessons from the IDE into Vim, and almost all of them collapse to a single keystroke: go to definition (gd, yes I had to setup language servers), find all references (gr), rename a symbol everywhere it lives (F2), the lightbulb quick-fix (F4), language-server autocomplete, a Problems panel for the squiggles (<leader>xx). I even bound the fuzzy file opener to Ctrl-P, the same key VS Code uses for the same thing.

It had quirks. A beat of lag sat between the keypress and the glyph, just long enough to notice; a one-keystroke edit became a command palette, a fuzzy match, an Enter; opening a 5,000-line file spun the fan up like a jet on a runway. The terminal lived in a cramped strip at the bottom, a few lines tall, where my whole workflow used to live. And the editor I tuned on my laptop was never quite the editor waiting on my desktop. Each one was small. Together they were ceremony.

By the end, though, it felt like maintaining a delicate garden. I would weed away panes so I could see more code and then they would sprout back up after a reboot.

My VS Code setup, the daily driver I switched to after a decade of Vim.

My typical workflow would be:

  1. My cursor sits in a function I just wrote. It calls an API I don't know, so I take a hand off the keyboard and mouse over it, waiting for the indexer to wake up.
  2. Right-click the symbol, scan the menu, click Go to Definition. A new tab opens in another file and scrolls me somewhere I didn't pick.
  3. That definition calls a bare helper. Hover it, wait for the tooltip, click Go to Definition again. A third tab piles on.
  4. Read the helper, then hunt the tab strip for the tab that was mine. Click back, mis-click a neighbor, correct.
  5. Back in my function, drag-select the wrong argument with the mouse, then fix the call.
  6. Click into the terminal pane, run the file, and wait for it to come back.

$HOME, sweet, $HOME

The thinking then is the same now: an IDE built for everyone optimizes you for average. This doesn't mean IDEs are bad or that people who use IDEs should feel bad (even John Carmack swears by Visual Studio, the full Microsoft IDE, not VS Code). It just means tooling is worth the investment when you live in code (even your IDE). Before Vim I used Xcode, Sublime Text, and Atom, and I still think Sublime was the GOAT back then.

With shell tools, I am able to craft the perfect environment for me. I like coding at the speed of thought. I feel more comfortable navigating, modifying, and manipulating the code all via the keyboard. I like completely automatable environments. I like being in control.

My Vim setup, back home in the terminal.

My typical workflow is:

  1. My cursor sits in a function I just wrote. It calls an API I don't know, so I read it in place. No mouse, nothing to wake up.
  2. gd jumps to the definition, and the jump is recorded so I can come back.
  3. That definition calls a bare helper. gd again, another hop on the jump list.
  4. Read the helper. Ctrl-o Ctrl-o walks the jump list back the way I came, onto my function.
  5. Back in my function, ci( swaps the argument in normal mode, no selection to drift.
  6. Ctrl-w to the tmux pane, recall the run command from history, Enter; the output prints in place.

Same six steps. Not one of them takes my hands off home row.

The AI of it All

Confession: FOMO was not the only reason I decided to try VS Code. My dotfiles had gone stale, mostly untouched for five years. Reviving them meant sinking days into a revamp before I could code again, or switching to VS Code and coding immediately. I chose the latter.

But then AI flipped that premise on its head. Particularly, Claude Code changed everything for me in 2025; can you spot when I started using it?

2017-Q116 2017-Q250 2017-Q37 2017-Q432 2018-Q118 2018-Q29 2018-Q313 2018-Q43 2019-Q113 2019-Q33 2019-Q45 2020-Q13 2020-Q218 2020-Q32 2020-Q47 2021-Q41 2022-Q21 2023-Q32 2024-Q17 2025-Q23 2025-Q3675 2025-Q423 2026-Q1166 2026-Q231

The cost of maintaining dotfiles was the cost of the tokens to do it.

But you may ask, with tools rapidly changing and shifting around AI today, will the terminal even matter tomorrow? In my opinion, it will matter even more.

The cost of writing code is minimal; the cost of reading code is still your reading speed. I read every line of code I ship to production, and you should too. The task has now shifted, but the thinking has not: the more time you spend reading code, the more time you should invest in tooling to assist you with it.

Reading code is not linear. You jump from your function to an API, to its helpers, and back. That's the motion both lists above describe, and the motion an editor lives or dies on.

How does Vim do this any better than an IDE? Because it's already designed for it.

Open a file in Vim, start typing, and nothing lands. You're in normal mode, where the keys are a vocabulary for moving, not typing: j/k/h/l step through the buffer, / jumps to anything you can name, gd lands on a definition and gr lists every reference, Ctrl-o walks the jump list back the way you came, % hops to the matching brace, ci( rewrites what is inside the parens with no selection to drift. Every one of those is a way to read. Writing is a mode you ask for, a deliberate i before a single character lands, and you leave it the moment you are done. The resting state of the editor is the thing you do most: looking at code that already exists, moving through it the way you read it, without waking an indexer, without ever reaching for the mouse.

An IDE is built the other way around. The cursor blinks at an insert point the second a file opens, primed to type, and every reading motion is a second layer bolted over that: a menu to find, a panel to define, a click to chase a reference. The default is write; reading is the thing you add. That used to be a preference. AI made it the whole job. When writing a line costs a sentence of English, the expensive part is no longer producing code, it is reading the code that came back, often code nobody at the keyboard wrote. Vim built its grammar for exactly that motion in the nineties, decades before the motion became the bottleneck.

You might object that agents change even this: you read their work as a diff in a review pane, not a buffer you move through. But a diff is a keyhole. To trust a change I still open the files it touches and follow what it calls, and that tracing lives in the editor, not the review pane. The pane shows me what moved; Vim is where I find out what it meant.

VS Code and Vim, spliced along a diagonal.

Whatever Works, Works

I tried something new. Don't drop everything you use and replace it with my stack; find one part of your workflow, see if it can be improved, and keep what works. If I had to pick somewhere to start, give Vim keybinds a go.

Learning a new editor is hard. Learning a key at a time is sustainable.

One afternoon it took me until lunch to notice my mouse had died. I'd been somewhere I didn't need it. There's still no place like $HOME.