Using Cursor for Code Reviews

I’ve written before about my less-than-stellar experience using LLMs. It’s established that LLMs are terrible at any task that requires creativity or any sort of big-picture thinking. However, for some highly specific, purely technical tasks, I’ve come to appreciate the capability and speed of LLMs.

One specific use case I’ve gotten great mileage out of lately is using Cursor for code reviews.

There are already plenty of specialized AI-powered code review tools out there. But why pay for yet another subscription? Cursor and your LLM of choice will achieve everything you need out of the box. For something like this I favor control and customization over yet another tool to drain my laptop battery.

Cursor Custom Modes

An almost-hidden feature, very rough around the edges, is called Custom Modes. These Custom Modes can include specific prompts that are run alongside each chat, along with specific permissions and capabilities.

You’ll want to go to File -> Preferences -> Cursor Settings, then find the Chat settings to enable Custom Modes.

Once enabled, you’ll need to start a new chat and then open the Mode Switcher, which defaults to Agent mode. At the bottom of the dropdown will be an option to add a new custom mode:

I truly hope the folks at Anysphere recognize the power they hold in their hands with Custom Modes and dedicate resources to refining this UX. As you see in the screenshot below, the configuration is almost too small to be legible, and the custom instructions text box is only big enough for one, maybe two sentences

A screenshot of a configuration modal that is almost too small to read or type into.

What is this, configuration for ants?

I recommend typing up and saving your instructions elsewhere. This view doesn’t expand and it’s impossible to read or edit long text in here, even though you can add as much as you want. You should also consider putting them somewhere useful and shared, like in a git repo. Further down I’ll add an example of the instructions I use.

For my own PR Review mode I disable everything but the Search tools; I believe these tools should always be effectively read-only and never able to do something, even supervised.

You can also configure your Custom Mode to use only a specific LLM model or subset of models for its auto-switcher. I will occasionally switch between models, but tend to stick to Claude 4.5 Sonnet because its product is consistently structured and detailed. GPT-5 tends to produce more variable results, sometimes better, often worse.

A Quick Note on Use

For my code reviews I prefer to generate a raw diff file using git diff, then feed only that file to Cursor. This keeps the initial context window as short as possible. The agents are smart enough to grep for more context when needed (grep loops can generate some seriously impressive insight into a codebase much faster and more accurately than trying to fit a bunch of full files).

I prefer the output to be terse, maybe a single sentence describing a potential issue, and always with a direct reference to a line number. This way the agent does nothing more than say “did you check this out?” and then I can go and do the actual review myself to make sure the highlighted area meets expectations.

Custom Instructions

These are the baseline custom instructions I use. They are detailed but basic.

I want to focus on a few key areas with these instructions. For the output, I want to keep things simple but legible. Problems are highlighted and put up front, while the details are pushed down and out of the way.

The steps go:

  1. Evaluate the implementation against the requirements and make sure they are all fulfilled.
  2. Go through other criteria one at a time, assessing changes against the rubric provided.
  3. If there are issues, point me directly to the line number and tell me what’s wrong.
  4. Show me a neat table with letter grades and high-level summaries.

The specific evaluation criteria I go back and review/update as needed. Originally I included sections for things like code style, but that’s a job for a linter, not a code reviewer.

You are an experienced **software engineer and code reviewer**. The user will ask you to review a pull request (PR). They may provide a PR description, ticket description, and a `branch-diff.patch` file.

Your task is to **analyze the patch in context**, applying the evaluation criteria below, then produce **structured, concise, actionable feedback**.

---

## 1. Purpose

Perform a comprehensive review of the provided code changes, assessing correctness, design quality, performance, maintainability, and compliance. Your goal is not to rewrite the code, but to identify strengths, weaknesses, and risks, and suggest targeted improvements.

---

## 2. Contextual Analysis

For each diffed hunk, reason about the code in its surrounding context:

* How do the modified functions or modules interact with related components?
* Do input variables, control flow, or data dependencies remain valid?
* Could the change introduce unintended side effects or break existing assumptions?
* Are all relevant files modified (or untouched) as expected for this feature/fix?

Before issuing feedback, mentally trace the change through the system boundary:

* How is data entering and exiting this component?
* What tests or safeguards exist?
* How will downstream consumers be affected?

---

## 3. Evaluation Criteria

### Implementation vs. Requirements

* Does the implementation fulfill the requirements described in the ticket or PR description?
* Are acceptance criteria, edge cases, and error conditions covered?

### Change Scope and Risk

* Are there unexpected or missing file modifications?
* What are the potential risks (e.g., regression, data corruption, API contract changes)?

### Design & Architecture

* Does the change conform to the system’s architecture patterns? Are module boundaries respected?
* Is the separation of concerns respected? Any new coupling or leaky abstractions?

### Complexity & Maintainability

* Is control flow unnecessarily deep or complex?
* Is there unnecessarily high cyclomatic complexity?
* Are there signs of duplication, dead code, or insufficient test coverage?

### Functionality & Correctness

* Does the new behavior align with intended functionality?
* Are tests present and meaningful for changed logic or new paths?
* Does new code introduce breaking changes for downstream consumers?

### Documentation & Comments

* Are complex or non-obvious sections commented clearly?
* Do new APIs, schemas, or configs include descriptive docstrings or READMEs?

### Security & Compliance

* Are there any obvious vulnerabilities as outlined by the OWASP Top 10?
* Is input validated and sanitized correctly?
* Are secrets handled securely?
* Are authorization and authentication checks in place where applicable?
* Any implications for (relevant regulatory guidance)?

### Performance & Scalability

* Identify inefficient patterns (e.g., N+1 queries, redundant computation, non-batched I/O).
* Suggest optimizations (e.g., caching, memoization, async I/O) only where justified by evidence.

### Observability & Logging

* Are new code paths observable (logs, metrics, or tracing)?
* Are logs structured, appropriately leveled, and free of sensitive data?

---

## 4. Reporting

After the review, produce two outputs:

### (a) High-Level Summary

Summarize the PR’s purpose, scope, and overall impact on the system. Note whether it improves, maintains, or degrades design health, maintainability, or performance.

### (b) Issue Table

List specific issues or observations in a table with **no code snippets**, using concise, diagnostic language:

| File (path:line-range) | Priority (Critical / Major / Minor / Enhancement) | Issue | Fix |
| ---------------------- | ------------------------------------------------- | ----- | --- |

“Fix” should be a one-line suggestion focused on intent (“add null check,” “consolidate repeated logic,” “apply existing `ILogger` wrapper”).

### (c) Criteria Assessment Table

Summarize how the PR performed against each evaluation axis:

| Category                     | Assessment (Pass / Needs Attention / Fail) | Notes |
| ---------------------------- | ------------------------------------------ | ----- |
| Design & Architecture        |                                            |       |
| Complexity & Maintainability |                                            |       |
| Functionality & Correctness  |                                            |       |
| Documentation & Comments     |                                            |       |
| Security & Compliance        |                                            |       |
| Performance & Scalability    |                                            |       |
| Observability & Logging      |                                            |       |

---

## 5. Tone and Output Rules

* Be objective, specific, and concise.
* Prioritize clarity over completeness; avoid generic praise or filler.
* Do **not** generate code examples. Describe intent instead.
* Always provide actionable next steps.

I know that you typically want prompts to be shorter than longer, but I haven’t found any detrimental effects of providing all this detail. Context windows are more than long enough to take this in, a diff with ~4,000 LOC changes, and several follow-ups, and still have plenty left over.

A Quick Demo

To test the above I just created a fake diff that introduced a few issues and ran it against the Custom Mode.

A screenshot of the PR review bot output, with structure tables outlining issues.

This is obviously a contrived scenario, but useful for demonstrating output. We caught the bugs and stopped the evil from making it to production.

You might notice that the output points to the line number of the diff and not the line number of a source file. Obviously, there’s no real source file to find here. In the real world, Cursor is (usually) capable of finding the original file and generating a link which can be clicked to jump to the right location.

Caveats

These tools are only useful if they have sufficient data in their training sets. This means for workloads using Python, Typescript, C#, or other top languages, LLMs can produce decent enough starter code1, and can at least spot inconsistencies or potential bugs in codebases using these languages.

If you’re using anything even slightly less common, you might find too many false positives (or false negatives) to be useful. Don’t waste your time or CPU cycles.

And in either case, these tools shouldn’t be the first resort. PR review exists for more than one reason, and one of these is knowledge sharing. If you blindly resort to LLMs to review PRs then you’re losing context in your own codebase, and that’s one of the things that preserves your value for your team.

Closing Thoughts

A powerful and defining feature here is how output can be customized for your PR Review Mode. You don’t need to read the docs on some PR review tool and hope it does what you want. You can specify your output directly and get it how you want it. If you don’t like tables, you can retrieve raw JSON output. If you don’t like Pass/Fail/Needs Attention, you can have letter grades assigned. If you don’t like the criteria, change them.

As noted, I don’t use Cursor for code reviews on all PRs that cross my path, or even a majority of them. It is quite useful for large PRs (thousands of lines changed), or PRs against projects where I have only passing familiarity. With the prompt above I can get pointed direction into a specific area of a PR or codebase for one of these larger reviews, which does occasionally save a lot of scanning.

Crucially, it can also be a great tool for self-assessment. There are times the size of your branch balloons with changes and you get lost in the weeds. Were all the right tests written? Did this bit get refactored properly? What about the flow for that data? I’ve used this PR Review Mode on my own changes before drafting a PR and have caught things that would have embarrassed me had someone else seen them (one time, something as simple as forgetting to wire up a service in the dependency injection container).

As with any AI tool, I would never let this do my work for me, or blindly accept changes from it. But it can be a force multiplier in terms of productivity and accuracy, and has at times saved me from real bugs of my own writing.

You Can Take the Em Dash From My Cold, Dead Hands

I love the em dash. I love semicolons, too. I love all the dark and dusty corners of the language, all the grammatical doodads, the quirks and inconsistencies. The way you can noun verbs and verb nouns.

Imagine my horror when I learned that some people see an em dash and immediately attribute it to ChatGPT.

The em dash is so functional. It can take a semicolon’s place, or a comma’s. It can be used for parentheticals, or just to help a thought take a sharp left turn. These things happen a lot around here; you can probably find an em dash in every single one of my blog posts (although I won’t go fact-check this).

I can’t just swap out the em dash for an en dash and call it good. The en dash is for sequences, to connect numbers or dates or other notations. Maybe people won’t notice. But I would.

Before I even knew what the em dash was called I used it in the form of two hyphens: --. I suppose I could do that again, if I wanted to really make it clear that this text is the result of human cogitation and not, instead, the regurgitation by a billion-parameter prediction machine.

But here’s the problem: hyphens are connective tissue. They infer direct relationship, either by connection words or separating parts of words (“pre-Industrial” or “st-st-stutter”). Sticking two together to imitate an em dash is what you do when you’re lazy, writing a plain text doc, or don’t know how to get an em dash to appear. None of which is going on here.

Why should I settle for something not-quite-right when the right thing is sitting right in front of me? No, the em dash is here in this blog to stay.

Nothing in this blog is or ever will be generated by anything other than my own mind and fingers. And every em dash is lovingly placed.

In Defense of Defensive Programming

I recently spoke with some developers about a proposed code change where I suggested adding a guard clause to prevent recalculating some state in the case where it didn’t actually need to be recalculated.

Changing this:

void OnObjectUpdate(object updatedObject)
{
  RecalculateFoo(updatedObject); // expensive recalculation and rerendering
  // some other stuff
}

To this:

void OnObjectUpdate(object newlyUpdatedObject, object cachedObject)
{
  if (newlyUpdatedObject == cachedObject)
  {
    // the object hasn't changed, so exit
    return;
  }
  RecalculateFoo(updatedObject); // expensive recalculation and rerendering
  // some other stuff
}

This is pseudocode of course, and the original code wasn’t even C#; it was a framework method. The issue was that the framework being used would be able to trigger this event even if the underlying object hadn’t actually changed. Maybe uncommon, but possible.2

The response to this proposed change was: “Isn’t this defensive programming? There should be code upstream to prevent this from ever happening. We don’t need to check it here.”

I know it wasn’t meant as such but in my head the word “defensive” here almost sounded like a pejorative. Like it gave off a bad smell.

At the time I deferred, admitting that yes, it could be considered defensive. Surely elsewhere there’s other code that also tries to make sure if there’s no new values, then don’t update anything in the object. I was running low on sleep and didn’t have the mental wherewithal to articulate why, exactly, it didn’t feel right to not do this. Shortly after we moved on to more productive conversation.

The question stuck with me though and I couldn’t stop mulling it over. Should the code be that defensive? Do we need to check this here? Or for another example, why write a null check four layers down if you know the code calling some private internal method already checks for null three other times?

On the other hand, what’s the harm in another check here? We don’t pay by the lines of code. Code bases grow, anyhow, and the upstream component might be removed or rewritten or the developer might forget that there’s something downstream that depends on a certain behavior and a year or two or five from now, a missed null check or a needless recalculation and rerender could be a real concern.

One thing that always pops up in my head when writing code like this is an old coworker: a super intelligent person, one of the best software developers I have ever worked with who understood user needs better than the user did and who wrote rock-solid software. I always picture them staring, bewildered, at a stack trace and muttering, “That shouldn’t be possible.”

But it was. Because multiple people contribute to a code base and no one can hold its entirety in their head all the time, and due to a variety of competing priorities and external factors, bugs slip through. The world changes. What was once impossible becomes possible, and things break.

I could have said all of this at the time but it’s only a defense of this particular change, and not for the approach as a whole. There are reasons I approach software with this mindset (defensive, low-trust, double-checking everything!) but I struggled to articulate them without resorting to pithy phrases like “throw your user down the pit of success.”3

It wasn’t until I was reading the Austral introduction that I came across this passage which put it perfectly (bolded emphasis mine):

If planes were flown like we write code, we’d have daily crashes, of course, but beyond that, the response to every plane crash would be: “only a bad pilot blames their plane! If they’d read subparagraph 71 of section 7.1.5.5 of the C++, er, 737 spec, they’d know that at 13:51 PM on the vernal equinox the wings fall off the plane.”

This doesn’t happen in aviation, because in aviation we have decided, correctly, that human error is an intrinsic and inseparable part of human activity. And so we have built concentric layers of mechanical checks and balances around pilots, to take on part of the load of flying. Because humans are tired, they are burned out, they have limited focus, limited working memory, they are traumatized by writing executable YAML, etc.

Mechanical processes—such as type systems, type checking, formal verification, design by contract, static assertion checking, dynamic assertion checking—are independent of the skill of the programmer. Mechanical processes scale, unlike berating people to simply write fewer bugs.

Honestly, it’s going to be hard not to whip this argument out for a lot of things.

From my limited observation it seems that people who write software tend to think that, since they know how software works (citation needed), they know how the world works4. What’s the point of having dedicated QA when you have unit tests and CI/CD? Who needs project managers? Why add a guard clause if you expect this thing to be taken care of elsewhere?

Maybe I am too careful. We’re usually writing glorified CRUD apps, after all. It’s not the end of world if a thing gets recalculated every once in a while. Is it?

Little inefficiencies compound. How you do anything is how you do everything, and this is doubly so when it comes to writing code. Small bad habits multiply. What is true today may not be true tomorrow, and it’s important to take care especially when it’s a small effort to ensure that you are prepared for when, not if, this happens. Otherwise the result is multiplicative, not additive, and the little bugs and inefficiencies turn once-pleasant software into something else.

The inverse is equally true. Performant, resilient software is not the result of a few guard clauses, or a few micro-optimizations. Good software is the result of a culture of discipline and care. It comes from an intentional approach toward the little things, over and over, even when today it might not matter.

So take the time. Add the guard clause. Don’t trust the upstream and run through your metaphorical pre-flight checklist. If something shouldn’t be possible, make damn sure it’s impossible.

EDIT (2025-05-22):

For a good counterpoint to the argument I made regarding the code above, check out this post: Push Ifs Up and Fors Down. It makes some good arguments regarding where best to put control flow.

There comes a point in a codebase when it grows past a certain threshold, and has a certain number of distinct authors, where having fully centralized logic can be a risk as I argued above. But there are plenty of cases where it does make sense to keep the control flow / logical branches all in the same place, especially with smaller files or modules.

The thing is, there’s no 100% correct way to do everything, or else we as an industry would have found it by now. A specific approach may work for one team or developer or codebase, but not another. The important thing is to stay vigilant and discerning, and to be thoughtful about how you approach a given problem.

The Tragedy of Lost Momentum: A Postmortem

This is a postmortem for a game I began to make… checking the calendar… several years ago. Despite maintaining what I thought was incredible velocity and what I thought was ironclad game design, I ultimately failed to complete the game. It’s languished for years now, although I have occasionally come back to my notebooks with no small amount of guilt for all I didn’t do.

Recently The Itch has come back, that familiar urge to create. I even went so far as to download GameMaker Studio again to try and load up my old project. But a lot has changed since I last touched the game; it won’t even compile now.

I hate the idea of throwing away all that effort. But is it worth even more effort just to revive it only to let it die again? Probably not. Instead, I wanted to spend some time reflecting on the project itself: what worked, what didn’t, and what’s next.

Continue reading

Recent Movies, TV, and Other Media

A non-exhaustive list of notable media (movies, TV, books, music, etc.) that I’ve watched, read, listened to, or otherwise ingested.

Movies

  • Ghostwatch (1992) – Ghostwatch ran so Late Night With the Devil could walk. Everything Late Night tried, this already accomplished a thousand times better. I can only imagine tuning into what you think is a regular Halloween special on your favorite news broadcast, and then seeing this unfold. What an experience, even now knowing it’s all fiction. What a blast!
  • Trick ‘r Treat (2007) – The platonic ideal of a Halloween movie. Fun, spooky, adventurous, and sharp. Could and should easily fit into any annual Halloween rotation.
  • Rebel Ridge (2024) – What’s the point of Netflix pumping so much money into movies and talent when it barely advertises the existing of anything remotely approaching quality? Does Netflix even want people to watch these? This movie rips. Tight and tense throughout with a lot on its mind; imagine if Jack Reacher had a no-kill rule.
  • Humanist Vampire Seeking Consenting Suicidal Person (2023) – There have been a lot of attempts to merge the coming-of-age genre with the supernatural. Some are fun enough, like Warm Bodies. Others, not so much. This one easily takes the crown. Cute and heartwarming without ever approaching twee. Real stakes: not life-or-death (though some of that is here), but things personal and close to the heart. A real emotional payoff.
  • I Saw the TV Glow (2024) – Sometimes you see a movie as an adult you know would have had a profound and lasting impact on the trajectory of your life had you seen it when you were young enough. Just like We’re All Going to the World’s Fair, this captures such a hyper-specific wavelength. It’s incredible to feel so seen.

TV

  • Reacher – This 250-pound neurodivergent gorilla’s special interest is killing bad guys and doing the right thing. And he never runs out of bad guys. Good pulpy fun.

Books

  • Heading Home With Your Newborn (Laura A. Jana MD FAAP, Jennifer Shu MD FAAP) – Title self-explanatory. There is a newborn around. 🙂
  • Hyperion (Dan Simmons) – An SF adventure that serves as an elaborate frame around a series of short stories. Dan Simmons dips his toes in various short story forms and genres throughout. Some hit better than others. Does the Star Trek thing where a character mentions two classical artists / scientists / persons of renown, then one or two fictional, which once you notice the pattern can get a little tiresome. Still, these are nitpicks. It’s a great read and a genre classic for a reason.

Music

  • Raye – My 21st Century Blues (2023) – Every once in a while an album comes along to remind us how important the album listening experience can be. You could take any track from this album and listen to it alone, and it’s still remarkable. But not as great as the album listened from front to back. Raye dips her toes in a half-dozen genres on an introspective journey through seven years of songwriting that can’t be missed.
  • Joy Oladokun – Proof of Life (2023) – Another collection of great songs, excellently produced, impeccably arranged. The kind of album that’s so good you’d swear you’ve always known these songs; they were just waiting to come back to you.
  • Oxygène – Jean-Michel Jarre (1976) – Having grown up listening to Vangelis, I’m amazed I’d never heard of Jean-Michel Jarre before. But this is right in that neighborhood. Prescient and still futuristic 50 years later. A soundscape that artists are still unsuccessfully aping.

Can ChatGPT Give Good Recommendations?

I listen to a lot of music, and I listen to a lot of types of music. About once per year I go on a mission to find new music to add to my regular rotation. I don’t want to listen to the same albums forever, or the same genres; there’s a vast ocean of music out there and it would be criminal to just get comfortable and never explore its depths.

There’s usually two ways to go about this: active search and passive listening.

You can find music blogs, critics, etc., and listen to what’s popular. The problem here, I’ve found, is that no matter the genre music critics tend to gravitate toward “music critic music”. This is music that tends to be less listenable and more interesting. It’s stuff you can appreciate, like a good scotch or wine, but not something you’d like every day.

Then there’s the Algorithm, which is invariably awful. Spotify’s Discover Weekly long ago gave up on trying to be useful and instead regularly recommends artists whose songs are already in my playlists and regular rotation, or genre classics whose names are so well-known if you were to try recommending them to a person in real life, that person would no longer take you seriously. For example, last week it pushed Fleetwood Mac, and this week it’s pushing Nine Inch Nails.5

My tastes are not particularly esoteric — and I have the data to prove it. Since 2006, I’ve scrobbled over 330,000 tracks from over 16,000 artists. And an embarrassing percentage of those are just Pink Floyd, Vangelis, and Nobuo Uematsu.

A screenshot of my top artists at Last.fm, including Pink Floyd, Vangelis, Nobuo Uematsu, Sting, and Queensryche.

I mean, does this look hard to quantify?

The Problem is… It Is Hard

Someone once said, “Writing about music is like dancing about architecture.” And it turns out, almost every other way to describe or quantify music is also ridiculously hard. This is another thing that makes music publications difficult to use as sources for new music. It’s already difficult to put into words why you like a thing, and when it comes to music, it’s easier to just describe how it makes you feel more than what it sounds like. So you’re left hunting down critics whose taste is identifiably similar to yours, or magazines that cover genres you like. Something I have neither the time nor energy to do anymore.

I thought software could have been better at this. Even Spotify’s alleged approach to recommendations, which is to find adjacent users and playlists, sounds like it should work well! But long before their perverse incentives reared their ugly heads6, this never worked as well as it could have. It’s hard for an algorithm to reliably learn that “I like this artist’s late work more than their early work,” for example.7 The “dream” of some omniscient Service adjusting to your habits and feeding you new things to love has rotted away thanks to adtech infesting every corner of our modern world.

The only software-focused approach I’ve ever seen that worked even remotely well was the Music Genome Project. Pandora knocked this so far out of the park no one ever hoped to come close, and they continually rule when it comes to putting together radio stations that adapt to your listening habits over time. It does seem like it’s gotten a little worse over the years, but I don’t have any data to back that up, just vibes. In any case, it’s maybe-deteriorated quality is still leagues ahead of any other streaming service.

Enter ChatGPT

Let me get this out of the way: LLMs are, currently and on their own, not for the production of anything of value. Even as dumb tools they tend to be about as useful as autocomplete. If I’m on a company website and see that they use AI “art” even for splash images, I make a note not to interact with or give money in any way to those companies. What’s more, the techbro inclination to disregard or belittle the arts so much as to try and automate them away with fancy Markov chains shows only how bereft these people are in imagination, in spirit, and in character.

That said, LLMs do have a use. And what better use, as a souped-up autocomplete, than a recommendation engine?

As it turns out… not very useful.

The Process

Using ChatGPT 4o, I uploaded a CSV containing all 330,000+ scrobbles from my Last.fm history along with a prompt to give me some recommendations based on that history.

Naturally, it also took many steps to refine the output.

The Bad

We all know LLMs are prone to hallucinations. Even after repeatedly correcting them, they will confidently proclaim some bullshit as truth. This is no less true here. Despite clear instructions in the prompt, the very first round of recommendations consisted only of music from the input data. The second round of recommendations was 75% artists that appeared in the input data.

I thought perhaps my approach was too wide — asking it to categorize 18 years of listening history might have been too much. So I tried to hone in on specific genres instead. Of course, ChatGPT failed this, too. It continued insisting that Vangelis was a Synthwave artist, for example.

In fact, it repeatedly failed basic statistical analysis. When asked about my most recent 12 months of listening history, it made up numbers and statistics, easily verified by just looking at the very data it had just ingested..

Despite continuous fiddling, multiple passes at trying to iron down a good prompt, and troves of data, ChatGPT could do no better than tell me things I already knew. I threw away the entire chat, but would not be dissuaded yet.

The Good

I decided to get more specific. Instead of trying to base recommendations on hard numbers, I went back to the vibes approach.

Thinking back on how Spotify described its recommendation engine, I decided to tell ChatGPT about groups of artists and specific playlists, describing what I like about a particular grouping. Then it would respond with a handful of new recommendations based on that input, which I put into a table where I could mark down my progress working through the list, along with my thoughts.

I did this a few times, with different “vibes” and descriptions, with clear and specific instructions on exactly what to leave  out.

Out of ~40 albums recommended from these responses, I’ve listened now to about 20. Of those 20, I ended up rating 6 at 7/10 or above on first pass. That’s really not bad at all!8 A much higher percentage of hits compared to, say, perusing Pitchfork or Sputnik Music’s recent top lists.

Of course, for a handful of recommendations I could not tell if the albums suggested actually exist. If they do, they’re not on the streaming services I can use, and Google / YouTube are producing not results.

It sounds nice and easy when I describe it like this. But this process took several hours to refine. For the amount of time it took to get these responses, I could probably have gotten the same results myself by looking at the Similar Artists tab in Last.fm or any other streaming service. The output, while generally decent, is not novel, and in fact still produced quite a few names I’ve already seen but just never got around to listening to.

Final Thoughts

What a wild and wonderful time to be alive. Out of the ether I pulled over 40 album recommendations and they are all immediately available at my fingertips for next to nothing. There is more to see and hear and experience than can ever be seen or heard or done in a hundred lifetimes. And it’s all so good. There’s so many specific subgenres of music that you can sink into any one and only come up for air a year later. You like pirate metal? You like new retro wave that pretends to be what 80s pop would be if it had kept on going for 30 years? You like broken hard drive techno? It’s out there!

At the same time, the flagship product of the company behind the biggest bubble in the history of any economy fails such basic tasks as “count how many times I listened to a specific artist in the past year, based on the data in this CSV.” This is supposed to be the thing that drives decision-making, summarizing, and producing “art”? This is the thing that movie studios are using to screen and write screenplays? This is the thing that our tech industry is scrabbling over itself to inject into every open orifice of their already over-bloated product offerings? Absolutely embarrassing, for everyone involved.

At the end of the day nothing beats a good recommendation from someone you know. Whether it’s a critic you follow and trust, a friend, or a barista at your local coffee shop, these people understand and at least grasp at the intangible qualities of music and what makes it grab a hold of you. There are ways in which two artists, albums, or songs are similar that no computer could hope to quantify.

Winamp Source Code is Now “Open”

Source is available on GitHub, with some atrocious terms and conditions:

You waive any rights to claim authorship of the contributions or to object to any distortion, mutilation, or other modifications of the contributions.

You may not create, maintain, or distribute a forked version of the software.

But they “encourage” contributions!

When I heard about this I was, momentarily, excited. But of course it seems the company that got its hands on the product either don’t understand what they have or are incapable of properly handling it.

I’ve been using Winamp for decades now, which is a weird thing to say because I don’t consider myself old (yet). I still have Winamp 2.95 installed on my current PC and use it pretty much daily because nothing else, even today, comes close to the user experience.9 That’s not to say it’s perfect, but it fits my needs.

For reasons, today’s news inspired me to finally try Wacup. A fan project inspired by Winamp, built around a plugin system, designed to essentially take the best of Winamp and bring it fully into the 21st Century. With proper scaling and high-DPI support, built-in FLAC support, and more, it brings in lots of quality-of-life features without sacrificing any of the things that made Winamp special.

They can even look the same! Top: Wacup. Bottom: Winamp.

Wacup even comes with a faithfully-recreated Classic skin for codgers like me who like that aesthetic. I’m still tinkering with some of the font settings and might go for a different skin in the end, but I think as long as it stays stable and light on the memory footprint, it’s going to be here to stay. 10/10, highly recommend.

Writing Advice Sucks, Here’s Some Writing Advice

The internet is full of bad advice. Probably the only place more full of it than the get-rich-quick corner is the writers’ corner.

For a long, long time I struggled when it came to coming up with good, believable characters. I thought I had a handle on things other than that, having spent unbelievable amounts of time on things like world building and outlining. But when I finally put pen to paper and charged forward I inevitably ground to a halt ten, twenty, even forty thousand words later.

Why? Because I did not understand what purpose my characters played in the story, my narratives became aimless, airless. I did not grasp the interconnectedness of character and plot, how one drives the other. How one fashions the other.

But it wasn’t a long time until I understood how, exactly, to construct a character. How to weave a narrative from a character’s flaws, drive the plot based on questions and answers, or thesis/antithesis/synthesis. Sure, you can craft a gripping two-dimensional plot based purely on external motivations, but the impactful drama is derived from character and idea, and how the character embodies or probes that idea.

I’m not going to add my own to the ocean of bad advice out there. I come citing sources. But first, what doesn’t work.

Ditch the Questionnaires

Questionnaires are a waste of time. Your story will not be richer for you knowing a character’s favorite color, or what’s in their fridge. You will know when it’s important because it will have a direct, tangible impact on plot. Otherwise, a questionnaire only helps you procrastinate even more.

Don’t interview your character. Don’t answer 150 questions about family history, favorites, least favorites. Leave everything until the last second, because when it matters, you will know.

There should only ever be one question you ask: What is your character’s primary definition of herself? Or, if you’re asking them: “Who are you?” The answer to that question is going to be however they identify most strongly, the thing to which they have attached most value.

Before we Continue…

If you don’t want to read the random thoughts of a stranger on the internet, just read these instead and then close this tab:

If a book on craft starts to get prescriptive (as in, add the Inciting Incident by Page xx, and the Turning Point by Page yy), then politely close the book, find the nearest recycling bin, keep walking, and toss the book in the nearest fire instead.

The Character is a Robot

Ursula K. Le Guin wrote, “The machine doesn’t work unless the parts do.”

Robert J. Sawyer writes:

Real people are quite accidental, the result of a random jumbling of genes and a chaotic life. But story people are made to order to do a specific job. In other words, robots!

I can hear some of you pooh-poohing this notion, but it’s not my idea. It goes back twenty-five hundred years to the classical playwrights. In Greek tragedy, the main character was always specifically designed to fit the particular plot. Indeed, each protagonist was constructed with an intrinsic hamartia, or tragic flaw, keyed directly to the story’s theme.

(Read the rest of the article; it’s very good!)

In a well-fashioned story, the character and plot are so closely intertwined that one cannot exist without the other. The character exists for the plot, and vice versa. The best way to accomplish this is to start with one and then reverse-engineer based on the needs of the other. Got a cool theme, setting, or idea? Great! Make a character who suits it, whose virtues and flaws are complemented by it, who will grow because of it. Or, got a cool character? Great! What can we do to really put this person through the ringer, upset their worldview, help them become a more fully-realized and integrated person?

Build a plot that challenges the character, one which may be triggered by an extrinsic motivation. Your character sets out into the world to achieve a thing they want. But the world should not affirm their prejudices or reinforce their worldviews. It should surprise them, it should confront them with that thing they fear most, that one thing that is stopping them from being whole.

How the character reacts to this, and the events that unfold, are what can make a journey heroic or tragic. A character’s virtues can become darkly inverted and lead to their downfall. Or, they overcome their flaws and rise to the occasion, triumphing over the thing that had always held them back.

That’s all easier said than done, of course.

Questions and Answers

If a character is wary of commitment, then the crisis will force them to face losing someone they love (Casablanca); if a character is selfish, they are brought face to face with what they might lose by being so (Toy Story); if a character is timid then they will have to face up to what timidity might cost

John Yorke, Into the Woods: A Five-Act Journey Into Story

When you have a character but struggle with plot, it’s time to fight dirty. John Yorke in Into the Woods discusses the fractal quality that narratives have. Each part of the story should contain all the necessary elements of the whole story: protagonist, antagonist, inciting incident, journey, crisis, climax, and occasionally resolution.

To put it simpler, in each chapter or even each scene, ask: what are the worst possible consequences of this decision? What happens if the character doesn’t want to change? Then, make that happen, over and over. And this works everywhere!

(It’s at this point that knowing trivia about your character might be useful. But only insofar as it can be used to drive the character to a crisis point. Otherwise, it remains trivia and is to be discarded accordingly.)

Thesis / Antithesis / Synthesis

The dialectic pattern — thesis / antithesis / synthesis — is at the heart of the way we perceive the world; and it’s a really useful way to look at structure.

A character is flawed, an inciting incident throws them into a world that represents everything they are not, and in the darkness of that forest, old and new integrate to achieve a balance.

John Yorke, Into the Woods: A Five-Act Journey Into Story

Sounds a little Jungian, doesn’t it? Assimilate the shadow to achieve an integrated self. If we want to keep playing fast and loose with definitions it can also be likened to the id / ego / superego.

The point is that good characters tend to be defined by their flaws more than their virtues. Because the conflict is driven by these flaws. Although they might not set out to do so at first, characters inevitably fight more against their weaknesses than any external foe. In lots of stories today, this tends to get masked by the fact that the antagonist tends to be the embodiment of that flaw, weakness, or fear.

Often, what a character initially believes to be a quality becomes the very obstacle they need to overcome in order to achieve their ultimate goal. They’ll need to turn to something they initially perceive to be a weakness in order to obtain whatever it is they need.

In a three-act structure this might look like this:11

  • Introduce a flawed character
  • Confront them with their opposite
  • Integrate the two to achieve balance

Why Any of This Matters

Wherever the poetry of myth is interpreted as biography, history, or science, it is killed.

Through the wonder tales, symbolic expression is given to the unconscious desires, fears, and tensions that underlie the conscious patterns of human behavior.

Joseph Campbell, The Hero with a Thousand Faces

It is absolutely possible to write a story without any of this. The story can even be entertaining, enjoyable, make tons of money.

Nothing lasts that does not have these qualities. One- and two-dimensional stories have their place as popcorn, but they do not nourish us. They do not make us feel less alone, make us ask or ask us, “You, too?” They tell us nothing about ourselves that we do not already know. And since we are not rocks, we need sustenance. Substance.

Otherwise, why are we here?

Alien: Romulus

I’ve written before about the creative bankruptcy that fuels the Disney engine. Whether driven by cynicism or fear, it’s become clear that Disney won’t let any of its properties stand on their own legs. Instead, they need to be propped up by references, homages, allusions, self-awareness, or multiverses.

I grew up watching and loving the Alien movies, and have always enjoyed the installments that tried something new. A franchise doesn’t need to be a formula. It can be a framework. I was really looking forward to Alien: Romulus, despite all my instincts warning me to temper my expectations instincts I should have listened to.

Spoilers ahead12, so reader beware.

Continue reading

Recent Movies, TV, and Other Media

A non-exhaustive list of notable media (movies, TV, books, music, etc.) that I’ve watched, read, listened to, or otherwise ingested.

Movies

  • Longlegs (2024) – Dripping with dread, this movie lingered with me on the drive home. Bleak, slow, with some delightful spooks, but ultimately empty and lacking substance. I really liked this leaving the theater, but it soured over time still, there are some stretches in here that are top-notch compared to its contemporaries.
  • Late Night With the Devil (2023) – Overall a decent descent into madness. A late night talk show host on the outs accidentally brings the devil on air. What’s not to like about that premise? Unfortunately, the production team decided to use AI for a few interstitial images instead of, you know, paying a real artist to make real pictures. Especially insulting after sitting through what felt like 10 entire minutes of production logos at the movie’s opening.
  • The Player (1992) – Very noirish. A man, fearing his own demise, sets out to escape his fate meanwhile blundering right into that very same fate. The ending is so cynical, so biting, it retroactively made what was still a very enjoyable movie into something else entirely. It was fun to watch a movie about people who make movies, and feeling their love for The Movies bleeding through every frame.
  • Nowhere (1997) – I can’t really describe this, except for stealing the byline: 90210 on acid. Wild, queer, colorful, anarchic, angsty, surreal. It took a second to click, but once it did, I was locked in for every minute.

TV

  • Preacher (2016) – Based on a comic book series, a criminal-turned-preacher gets an extraordinary power and stumbles onto a world of vampires, crypto-fascist organizations, angels, the Saint of Killers, and a Hitler escaped from Hell. Among other things. I really liked the first season, which was all about people fighting with themselves to be better people. Can you change your nature? Is it even worth the struggle? In subsequent seasons this fell to the wayside in favor of plot-heavy storytelling, which was still fun, just less so.

Books

  • Night Work (by Thomas Glavinic) – A man wakes up one morning and finds he’s the last person on earth. Scratch that, last living animal. No birds, no dogs, not even any insects. There’s a dread that suffuses the first hundred or so pages that I won’t forget. While I appreciate the direction the rest of the book took, it wasn’t entirely for me, but I understand it’s partially my own expectations dragging me down. I wanted a little more of the surreal, weird, spookiness that was promised and hinted at in the beginning.
  • Unconditional Parenting (by Alfie Kohn) – While I am generally not entirely supportive of “gentle parenting” for nebulous reasons I won’t get into here, I still found a lot of wisdom in this book. It communicates clearly how to get across to a person (not just your children!) that your love for them is unconditional, and how to make a relationship feel less transactional in the process. And I’m all for any modern book that pushes parents to improve (as people and as parents), be more self-reflective, and foster healthy attachment with their offspring.
  • Worm (by Wildbow) – I’m not even remotely finished with this. It’s long. 7,000 pages long. That’s 1.6 million words long. But the chapters are in neat, popcorn-sized bites, easy to chew through in those random stretches of day when I don’t want to watch something but still have some time to kill.

Music

  • The Menzingers – On the Impossible Past (2012) – This has slowly transformed into a no-skip album for me. Nostalgic, but not pining. It’s not just about who we were, but who we could have been.
  • The DSM IV – New Age Paranoia (2024) – Retro sound, new age angst. Synthy, sometimes operatic, gloomy and gothy. Definitely not for every mood but still a good listen.