<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:source="https://source.scripting.com/">
	<channel>
		<title>Martin Emde</title>
		<description>Blog posts by Martin Emde</description>
		<link>https://martinemde.com</link>
		<atom:link href="https://martinemde.com/rss.xml" rel="self" type="application/rss+xml" />
		<source:self>https://martinemde.com/rss.xml</source:self>
		<pubDate>Sat, 21 Mar 2026 12:00:00 GMT</pubDate>
		<lastBuildDate>Sun, 09 Aug 2026 11:21:13 GMT</lastBuildDate>
    
		<item>
			<title>Vestigial Mastery</title>
			<description>I was 12 when I learned VIm. It&apos;s almost useless now.</description>
			<link>https://martinemde.com/blog/vestigial-mastery</link>
			<guid isPermaLink="true">https://martinemde.com/blog/vestigial-mastery</guid>
			<pubDate>Sat, 21 Mar 2026 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>I used to be really good at editing code.</p>
<p>I was 12 when I learned VIm. I asked my dad "how do I jump to a line number in a file?"</p>
<blockquote>
<p>If you open it in vi [instead of pico] I can show you. It's just <code>:123</code>.</p>
</blockquote>
<p>Amazing! So I learned VIm and I used it for 30 years. It's the only editor I ever got good at.</p>
<p>It's almost useless now.</p>
<p>Thirty years of editing code. I typed every character by hand until Co-Pilot. Instead of a single word, the ghost sometimes typed the rest of the line. I switched (with the VIm plugin, of course). Amazing!</p>
<p>Progressively less and less of my code was typed character by character. What used to be a macro or a clever motion became <code>Tab Tab Tab</code>. Coding became comments for the ghost to fill in the rest. Inevitably it was a little wrong, but I was good at editing code. <code>AlmostRight</code> turned to <code>Quality</code> with a few VIm motions. I was a lot faster this way.</p>
<p>Then the chat on the side got better. The end was near. It was more efficient to ask for the fix than find and edit the code. My wrists, always a little sore, savored the fewer key presses.</p>
<p>I typed prose into the chat.</p>
<p>I stopped writing code.</p>
<p>I stopped editing code.</p>
<p>I stopped using VIm.</p>
<p>Mastery intact and barely a reason to use it.</p>
<p>I still use it sometimes. I go through the VIm motions of editing code. Obsidian, bless its code, and the lovingly thorough emulation in Claude Code, are now my outlet for VIm. The skills and mastery of 30 years of <code>hjkl</code> and <code>:%s/vim/claude/</code> have become a simple trick now that comforts my muscle memory. The motions are a permanent part of my brain, but they don't make me better at my job. Hours of modal efficiency are no more than a few minor minutes of savings now.</p>
<p>That era of hand editing has ended. I wouldn't trade my new powers for the sore wrists of the past. I was good at text editing. I still am. It feels good to have mastery. It just doesn't matter anymore.</p>
<p>My mastery of VIm has become vestigial.</p>
<p>:wq</p>
]]></content:encoded>
			<source:markdown><![CDATA[I used to be really good at editing code.

I was 12 when I learned VIm. I asked my dad "how do I jump to a line number in a file?"

> If you open it in vi [instead of pico] I can show you. It's just `:123`.

Amazing! So I learned VIm and I used it for 30 years. It's the only editor I ever got good at.

It's almost useless now.

Thirty years of editing code. I typed every character by hand until Co-Pilot. Instead of a single word, the ghost sometimes typed the rest of the line. I switched (with the VIm plugin, of course). Amazing!

Progressively less and less of my code was typed character by character. What used to be a macro or a clever motion became `Tab Tab Tab`. Coding became comments for the ghost to fill in the rest. Inevitably it was a little wrong, but I was good at editing code. `AlmostRight` turned to `Quality` with a few VIm motions. I was a lot faster this way.

Then the chat on the side got better. The end was near. It was more efficient to ask for the fix than find and edit the code. My wrists, always a little sore, savored the fewer key presses.

I typed prose into the chat.

I stopped writing code.

I stopped editing code.

I stopped using VIm.

Mastery intact and barely a reason to use it.

I still use it sometimes. I go through the VIm motions of editing code. Obsidian, bless its code, and the lovingly thorough emulation in Claude Code, are now my outlet for VIm. The skills and mastery of 30 years of `hjkl` and `:%s/vim/claude/` have become a simple trick now that comforts my muscle memory. The motions are a permanent part of my brain, but they don't make me better at my job. Hours of modal efficiency are no more than a few minor minutes of savings now.

That era of hand editing has ended. I wouldn't trade my new powers for the sore wrists of the past. I was good at text editing. I still am. It feels good to have mastery. It just doesn't matter anymore.

My mastery of VIm has become vestigial.

:wq]]></source:markdown>
		</item>
		<item>
			<title>Claude Code commands deprecated in favor of skills</title>
			<description>Anthropic pushed out a change to the docs that deprecates commands, pointing everyone te use the new improved skills with support for most of the same feature set.</description>
			<link>https://martinemde.com/blog/claude-code-commands-deprecated</link>
			<guid isPermaLink="true">https://martinemde.com/blog/claude-code-commands-deprecated</guid>
			<pubDate>Thu, 22 Jan 2026 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>Today, I noticed I could no longer find the <a href="https://code.claude.com/docs/en/slash-commands" title="The old link to slash command documentation">Slash commands</a> documentation that I was using.
The link is still named Slash commands, but it renders the <a href="https://code.claude.com/docs/en/skills">Skills</a> docs.</p>
<p>I had been looking closely at Claude Code's Skills and Commands lately as I build <a href="https://github.com/martinemde/skillet" title="Run claude skills as beautiful shell scripts">skillet</a>, and all today I kept trying to remind myself what was different between them. Sure enough, if you read the big blue box at the top of the docs, it says:</p>
<blockquote>
<p>[!NOTE] For built-in commands like /help and /compact, see <a href="https://code.claude.com/docs/en/interactive-mode#built-in-commands">interactive mode</a>.</p>
<p>Custom slash commands have been merged into skills. A file at .claude/commands/review.md and a skill at .claude/skills/review/SKILL.md both create /review and work the same way. Your existing .claude/commands/ files keep working. Skills add optional features: a directory for supporting files, frontmatter to <a href="https://code.claude.com/docs/en/skills#control-who-invokes-a-skill">control whether you or Claude invokes them</a>, and the ability for Claude to load them automatically when relevant.</p>
</blockquote>
<p>They merged some of the command-only features into skills.
You can now use <code>hooks</code> in skills which were previously command only, and
they added support for injecting command output into the prompt.
Check out this example from the docs (note the <code>!</code> preceding the inline commands).</p>
<pre><code class="language-md">---
name: pr-summary
description: Summarize changes in a pull request
context: fork
agent: Explore
allowed-tools: Bash(gh:*)
---

## Pull request context

- PR diff: !`gh pr diff`
- PR comments: !`gh pr view --comments`
- Changed files: !`gh pr diff --name-only`

## Your task

Summarize this pull request...!`gh pr diff`
</code></pre>
<p>The inline commands will be processed and interpolated before evaluating the skill.</p>
<p>One thing that is still missing is support for numbered arguments, like <code>$1</code> and <code>$2</code>.
These were complicated, and may have been under-utilized.
I chose to skip it myself when I went to implement command support.</p>
<p>I'm excited to see this merger. It seemed inevitable and their timing, at least for me, is impeccable.
Skillet will keep command support for now, as commands still exist, but I suggest
migrating all your commands now to take advantage of the newly combined set of features.</p>
]]></content:encoded>
			<source:markdown><![CDATA[Today, I noticed I could no longer find the [Slash commands][slash-commands] documentation that I was using.
The link is still named Slash commands, but it renders the [Skills](https://code.claude.com/docs/en/skills) docs.

I had been looking closely at Claude Code's Skills and Commands lately as I build [skillet][skillet], and all today I kept trying to remind myself what was different between them. Sure enough, if you read the big blue box at the top of the docs, it says:

> [!NOTE] For built-in commands like /help and /compact, see [interactive mode](https://code.claude.com/docs/en/interactive-mode#built-in-commands).
>
> Custom slash commands have been merged into skills. A file at .claude/commands/review.md and a skill at .claude/skills/review/SKILL.md both create /review and work the same way. Your existing .claude/commands/ files keep working. Skills add optional features: a directory for supporting files, frontmatter to [control whether you or Claude invokes them](https://code.claude.com/docs/en/skills#control-who-invokes-a-skill), and the ability for Claude to load them automatically when relevant.

They merged some of the command-only features into skills.
You can now use `hooks` in skills which were previously command only, and
they added support for injecting command output into the prompt.
Check out this example from the docs (note the `!` preceding the inline commands).

```md
---
name: pr-summary
description: Summarize changes in a pull request
context: fork
agent: Explore
allowed-tools: Bash(gh:*)
---

## Pull request context

- PR diff: !`gh pr diff`
- PR comments: !`gh pr view --comments`
- Changed files: !`gh pr diff --name-only`

## Your task

Summarize this pull request...!`gh pr diff`
```

The inline commands will be processed and interpolated before evaluating the skill.

One thing that is still missing is support for numbered arguments, like `$1` and `$2`.
These were complicated, and may have been under-utilized.
I chose to skip it myself when I went to implement command support.

I'm excited to see this merger. It seemed inevitable and their timing, at least for me, is impeccable.
Skillet will keep command support for now, as commands still exist, but I suggest
migrating all your commands now to take advantage of the newly combined set of features.

[skillet]: https://github.com/martinemde/skillet 'Run claude skills as beautiful shell scripts'
[slash-commands]: https://code.claude.com/docs/en/slash-commands 'The old link to slash command documentation'
[skills]: https://code.claude.com/docs/en/skills 'The new skills documentation']]></source:markdown>
		</item>
		<item>
			<title>Fast claude @file suggestion in BIG repositories</title>
			<description>Faster file suggestions in monorepos using Claude code @file suggestions based on git and an auto refreshing cache.</description>
			<link>https://martinemde.com/blog/fast-claude-file-suggestion-in-big-repos</link>
			<guid isPermaLink="true">https://martinemde.com/blog/fast-claude-file-suggestion-in-big-repos</guid>
			<pubDate>Tue, 13 Jan 2026 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>At <a href="https://gusto.com" title="Gusto - #1 Rated HR Platform - (also where I work)">Gusto</a> we have some <em>big</em> repositories.
Many tools struggle with large codebases and Claude Code is no exception.</p>
<p>File suggestion, when you type <code>@rea</code> and it suggests <code>@README.md</code>, is one area where the number of files in a project has an outsize impact on a UI interaction. Filtering 110,000 files every time you type a character can make claude choppy and slow.</p>
<p>Anthropic has offered <a href="https://code.claude.com/docs/en/settings#file-suggestion-settings" title="Anthropic Help: Claude Code File Suggestion settings">a worksround</a> for this problem by providing the <code>fileSuggestion</code> setting to allow customization for big repositories.</p>
<blockquote>
<p>The built-in file suggestion uses fast filesystem traversal, but large monorepos may benefit from project-specific indexing such as a pre-built file index or custom tooling.</p>
</blockquote>
<p>I hacked my own solution for our giant repo with claude:
<a href="https://raw.githubusercontent.com/martinemde/dotfiles/main/home/dot_claude/executable_file-suggestion.sh" title="Link to current version">file-suggestion.sh</a> is a custom file-suggestion command which builds a cache from <code>git ls-files</code>, if available.
It falls back to <code>fd</code> or <code>find</code> outside of a git repository.
These results are filtered with <code>ripgrep</code> and <code>fzf</code>, allowing fast fuzzy searching.</p>
<p>As a surprise bonus, using a custom script seems to fix the choppiness and UI lag that plagues big repos.
My guess is that the need to spawn a custom script forces the app to run the filtering async.
In a repo like <a href="https://gusto.com" title="Gusto - #1 Rated HR Platform - (also where I work)">Gusto</a>'s core product, this can lag input considerably to where your keypresses stop rendering. Deleting characters is even worse, slowing to a crawl as the search space expands.</p>
<p><em>Hey, claude code devs, if you're reading:</em></p>
<pre><code>Move the file suggestion filtering off the main
UI thread so suggestions are returned async,
following an approach similar to spawning a
process for a custom fileSuggestion command.
</code></pre>
<p>If you want to try my script, make sure you have <code>ripgrep</code>, <code>fzf</code> and optionally <code>fd</code> installed.</p>
<p>Grab the script below (or point your own claude at it and ask for a version that works for you). You can also view the <a href="https://raw.githubusercontent.com/martinemde/dotfiles/main/home/dot_claude/executable_file-suggestion.sh" title="Link to current version">latest version</a> if I didn't already break the link by the time you get here.</p>
<pre><code class="language-bash">curl -o ~/.claude/file-suggestion.sh https://raw.githubusercontent.com/martinemde/dotfiles/36f670bda583065f634f1e83c4a195b9ac39c17b/home/dot_claude/executable_file-suggestion.sh
chmod +x ~/.claude/file-suggestion.sh
</code></pre>
<p>Then add the following to <code>~/.claude/settings.json</code> (ensure the path matches and the file is executable).</p>
<pre><code class="language-json">  "fileSuggestion": {
    "type": "command",
    "command": "~/.claude/file-suggestion.sh"
  }
</code></pre>
<p>Restart Claude Code, then test out the search by typing an <code>@</code> and the start of a file. Fuzzy matching should let you match any fuzzy path after the first slash.</p>
<p>On our biggest repo, this drops the search time to about <strong>62ms</strong> compared to around <strong>1000ms</strong> without the index (one full second per bounce).
The script watches for the <code>.git/index</code> or <code>.git/HEAD</code> to be newer than the cache file and automatically refreshes the cache.</p>
<p>The cache is stored in the project's <code>.claude/</code> directory, so you'll want to ignore the <code>.claude/file-suggestions.cache</code> in your global gitignore file.</p>
<p>In order to attain speed and retain fuzzy matching, it pre-filters based on the first directory segment using ripgrep to cut down on results.
This allows you to type <code>pac/payroll</code> to find <code>packs/products/payroll/...</code> but if you type
<code>pks/payroll</code> or <code>papay</code> it may not return any results.
This is a trade-off for speed that I'd don't love since I was expecting neovim level fuzzy find.</p>
<p>I may experiment with dropping the pre-filter and accepting the slower results just to get more reliable fuzziness.
Please let me know if you find a better way around it.</p>
]]></content:encoded>
			<source:markdown><![CDATA[At [Gusto][gusto] we have some _big_ repositories.
Many tools struggle with large codebases and Claude Code is no exception.

File suggestion, when you type `@rea` and it suggests `@README.md`, is one area where the number of files in a project has an outsize impact on a UI interaction. Filtering 110,000 files every time you type a character can make claude choppy and slow.

Anthropic has offered [a worksround][file-suggestion] for this problem by providing the `fileSuggestion` setting to allow customization for big repositories.

> The built-in file suggestion uses fast filesystem traversal, but large monorepos may benefit from project-specific indexing such as a pre-built file index or custom tooling.

I hacked my own solution for our giant repo with claude:
[file-suggestion.sh][latest] is a custom file-suggestion command which builds a cache from `git ls-files`, if available.
It falls back to `fd` or `find` outside of a git repository.
These results are filtered with `ripgrep` and `fzf`, allowing fast fuzzy searching.

As a surprise bonus, using a custom script seems to fix the choppiness and UI lag that plagues big repos.
My guess is that the need to spawn a custom script forces the app to run the filtering async.
In a repo like [Gusto][gusto]'s core product, this can lag input considerably to where your keypresses stop rendering. Deleting characters is even worse, slowing to a crawl as the search space expands.

_Hey, claude code devs, if you're reading:_

```
Move the file suggestion filtering off the main
UI thread so suggestions are returned async,
following an approach similar to spawning a
process for a custom fileSuggestion command.
```

If you want to try my script, make sure you have `ripgrep`, `fzf` and optionally `fd` installed.

Grab the script below (or point your own claude at it and ask for a version that works for you). You can also view the [latest version][latest] if I didn't already break the link by the time you get here.

```bash
curl -o ~/.claude/file-suggestion.sh https://raw.githubusercontent.com/martinemde/dotfiles/36f670bda583065f634f1e83c4a195b9ac39c17b/home/dot_claude/executable_file-suggestion.sh
chmod +x ~/.claude/file-suggestion.sh
```

Then add the following to `~/.claude/settings.json` (ensure the path matches and the file is executable).

```json
  "fileSuggestion": {
    "type": "command",
    "command": "~/.claude/file-suggestion.sh"
  }
```

Restart Claude Code, then test out the search by typing an `@` and the start of a file. Fuzzy matching should let you match any fuzzy path after the first slash.

On our biggest repo, this drops the search time to about **62ms** compared to around **1000ms** without the index (one full second per bounce).
The script watches for the `.git/index` or `.git/HEAD` to be newer than the cache file and automatically refreshes the cache.

The cache is stored in the project's `.claude/` directory, so you'll want to ignore the `.claude/file-suggestions.cache` in your global gitignore file.

In order to attain speed and retain fuzzy matching, it pre-filters based on the first directory segment using ripgrep to cut down on results.
This allows you to type `pac/payroll` to find `packs/products/payroll/...` but if you type
`pks/payroll` or `papay` it may not return any results.
This is a trade-off for speed that I'd don't love since I was expecting neovim level fuzzy find.

I may experiment with dropping the pre-filter and accepting the slower results just to get more reliable fuzziness.
Please let me know if you find a better way around it.

[gusto]: https://gusto.com 'Gusto - #1 Rated HR Platform - (also where I work)'
[file-suggestion]: https://code.claude.com/docs/en/settings#file-suggestion-settings 'Anthropic Help: Claude Code File Suggestion settings'
[latest]: https://raw.githubusercontent.com/martinemde/dotfiles/main/home/dot_claude/executable_file-suggestion.sh 'Link to current version']]></source:markdown>
		</item>
		<item>
			<title>Ghostty Focus and Blur Shaders</title>
			<description>How to create dynamic focus and unfocus state shaders in Ghostty with the new iFocus and iTimeFocus uniforms available in Ghostty prerelease.</description>
			<link>https://martinemde.com/blog/ghostty-focus-shaders</link>
			<guid isPermaLink="true">https://martinemde.com/blog/ghostty-focus-shaders</guid>
			<pubDate>Sat, 10 Jan 2026 12:00:00 GMT</pubDate>
			<enclosure url="https://martinemde.com/images/blog/ghostty-focus-shaders.png" type="image/jpeg" length="0"/>
			<content:encoded><![CDATA[<img src="https://martinemde.com/images/blog/ghostty-focus-shaders.png" alt="Ghostty Focus and Blur Shaders" /><p>What if you could have that sick Ghostty CRT effect but only on unfocused windows,
or a rad flaming cursor highlight but only once when you focused the window?</p>
<h2>Introducing the iFocus and iTimeFocus uniforms</h2>
<p>My recent <a href="https://github.com/ghostty-org/ghostty/pull/10130" title="Add iFocus and iTimeFocus shader uniforms">contribution</a> to Ghostty enables new focus-based shaders.
You can use these new input variables, <code>iFocus</code> and <code>iTimeFocus</code>, known as "uniforms" in Shadertoy GLSL, to allow shaders to react to focus and blur events.</p>
<p><em>Note: Until the next Ghostty release, you'll need to <a href="https://ghostty.org/docs/install/pre" title="Docs: Install Ghostty Prerelease">install a recent Ghostty prerelease version</a> to try this out.</em></p>
<h2>Example: CRT Shader on Blur</h2>
<p>Here's the shader I use in Ghostty for unfocused surfaces.</p>
<p>Try clicking into and then outside of the "terminal" (yes it's upside down).
You should see a CRT-style effect that clears when you focus the terminal.</p>
<p>{#if blurDemoShaders.length > 0}

{:else}</p>
<p>Here I'm emulating Ghostty's <code>iFocus</code> uniform to apply a CRT shader only when the canvas is unfocused.
It may not render in every browser.
Try it directly on my site with a modern browser or zoom in really close on your phone.</p>
<h2>Example: Focus Animations</h2>
<p>Here we emulate the <code>iTimeFocus</code> and <code>iFocus</code> uniforms to create a cursor highlight effect.</p>
<p>Click into the "terminal" and a cursor focus zoom effect triggers.
Click around more, the effect only plays once.
Defocus the "terminal" then focus it again and you'll replay the focus animation.</p>
<p>{#if focusDemoShaders.length > 0}

{:else}</p>
<p>If you'd like to play with these shaders more, check out my <a href="https://martinemde.com/shaders" title="Martin&#x27;s Ghostty Shaders">Ghosty Shaders</a> demo page.</p>
<p>Toggle between the shaders using the menu button in the top-left of the demo canvas, or look at the debug overlay (bottom right) for uniform details.</p>
<h2>How it works: Focus/Blur Effects</h2>
<p>The <code>iFocus</code> uniform is an <code>int</code> set to either <code>0</code> (blurred) or <code>1</code> (focused).
This uniform is useful for controlling whether a shader renders on focused or blurred surfaces (panes or windows).</p>
<p>To save resources, Ghostty shaders do not run on every frame for unfocused surface.
This is generally a good thing and saves resources, except sometimes unfocused surfaces receive frames anyway.
This can cause stutters and skips in a paused animation or cursor effect on blurred surfaces.</p>
<p>With <code>iFocus</code> we can intentionally apply blurred styles to unfocused surfaces or run shaders only on focused frames.
This fixes the stutters and skips by allowing the shader to ignoring these deceptive frames.</p>
<h3>Example Usage</h3>
<p>The following <code>if</code> statement can be added to any shader to make it render only when blurred. You can
check <code>if (iFocus == 1)</code> to run something only when focused instead.</p>
<pre><code class="language-glsl">void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord.xy / iResolution.xy;

    // Early exit when focused
    if (iFocus == 0) {
        // Render normal texture when
        fragColor = texture(iChannel0, uv);
        return;
    }

    // The rest of the shader goes here...
}
</code></pre>
<p>This example disables all shader effects when the surface is focused, allowing unfocused states such as CRT scanlines, vignettes, shadows, or faded colors that render only on the blurred surfaces.</p>
<p>On the other hand, if you invert the logic you can run something only when the surface is focused,
which I have used to make my cursor effects only appear on focused surfaces.</p>
<h2>Focus Resume Animations</h2>
<p>The <code>iTimeFocus</code> uniform is a <code>float</code>, set to the <code>iTime</code> when the surface last received focus.
We can use this to render effects that need to animate when the surface is focused.</p>
<p>Here's an example of how to use <code>iTimeFocus</code> to create a resume animation.</p>
<pre><code class="language-glsl">const float PULSE_DURATION = 0.15;  // Animation duration in seconds

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;

    // Quick exit: only run during active focus animation
    float timeSinceFocus = iTime - iTimeFocus;
    if (iFocus == 0 || timeSinceFocus &#x3C; 0.0 || timeSinceFocus > PULSE_DURATION) {
        fragColor = texture(iChannel0, uv);
        return;
    }

    // Render focus resume animation...
}
</code></pre>
<p>In this example, the shader checks how much time has passed since the surface received focus.
If the surface is blurred, or if the focus animation duration has passed, the shader exits early to
save resources.</p>
<p>You can see and install my <a href="https://github.com/martinemde/dotfiles/blob/main/home/dot_config/ghostty/shaders/focus_cursor.glsl" title="A shader that zoom fades the cursor on focus">focus-cursor</a> shader for a complete example.</p>
<h2>Rendering shaders on blurred surfaces</h2>
<p>Ghostty shaders mostly don't receive frames when blurred.
This is for performance reasons and can be disabled with the <code>custom-shader-animation=always</code> config option.</p>
<p>However, it's inconsistent. Sometimes defocused surfaces get frames when a mod key is press (like
command or alt) or when the mouse moves over a link.
With <code>iFocus</code> you can intentionally filter these frames out to avoid ugly stutters.</p>
<p>So far it seems blurred surfaces reliably receive at least one defocused frame to render the defocused state.
While you can't ensure that an animation could complete on blur (without <code>custom-shader-animation=always</code>), you can at least ensure that the defocused state is rendered once.</p>
<p>Therefore, I would suggest that all shaders should include this gating <code>if</code> statement unless they are intended to run <code>always</code>.
It improves performance and polish on both focused state cursor animations and blurred state rendering.</p>
<h2>Installing Ghostty prerelease</h2>
<p>Again, if you want to use these shaders, you'll need to <a href="https://ghostty.org/docs/install/pre" title="Docs: Install Ghostty Prerelease">install the prerelease</a> version of Ghostty.
It might be glitchy (I immediately submitted another bug fix after this feature because the dev build had a problem)</p>
]]></content:encoded>
			<source:markdown><![CDATA[<script lang="ts">
  import ShaderCanvas, { type ShaderInfo } from '$lib/components/ShaderCanvas.svelte';
  import { onMount } from 'svelte';

  let cursorColor = $state<[number, number, number, number]>([0.953, 0.722, 0.529, 1.0]); // Catppuccin Peach
  let prevCursorColor = $state<[number, number, number, number]>([0.71, 0.733, 0.98, 1.0]); // Catppuccin Lavender

  // Shaders for blur demo (CRT enabled, focus cursor disabled)
  let blurDemoShaders = $state<ShaderInfo[]>([]);

  // Shaders for focus demo (focus cursor enabled, CRT disabled)
  let focusDemoShaders = $state<ShaderInfo[]>([]);

  // Load shaders on mount
  onMount(async () => {
    try {
      const [blurResponse, focusResponse, cursorRectResponse] = await Promise.all([
        fetch('/shaders/blur_crt.glsl'),
        fetch('/shaders/focus_cursor.glsl'),
        fetch('/shaders/cursor_rectangle.glsl')
      ]);

      const blurCode = blurResponse.ok ? await blurResponse.text() : null;
      const focusCode = focusResponse.ok ? await focusResponse.text() : null;
      const cursorRectCode = cursorRectResponse.ok ? await cursorRectResponse.text() : null;

      // Blur demo: CRT enabled, others disabled
      if (blurCode && focusCode && cursorRectCode) {
        blurDemoShaders = [
          { name: 'Cursor Rectangle', code: cursorRectCode, enabled: true },
          { name: 'Focus Cursor', code: focusCode, enabled: false },
          { name: 'CRT Blur', code: blurCode, enabled: true },
        ];
      }

      // Focus demo: Focus cursor enabled, CRT disabled
      if (blurCode && focusCode && cursorRectCode) {
        focusDemoShaders = [
          { name: 'Cursor Rectangle', code: cursorRectCode, enabled: true },
          { name: 'Focus Cursor', code: focusCode, enabled: true },
          { name: 'CRT Blur', code: blurCode, enabled: false }
        ];
      }
    } catch (error) {
      console.error('Failed to load shaders:', error);
    }
  });
</script>

What if you could have that sick Ghostty CRT effect but only on unfocused windows,
or a rad flaming cursor highlight but only once when you focused the window?

## Introducing the iFocus and iTimeFocus uniforms

My recent [contribution][pr] to Ghostty enables new focus-based shaders.
You can use these new input variables, `iFocus` and `iTimeFocus`, known as "uniforms" in Shadertoy GLSL, to allow shaders to react to focus and blur events.

_Note: Until the next Ghostty release, you'll need to [install a recent Ghostty prerelease version][prerelease] to try this out._

## Example: CRT Shader on Blur

Here's the shader I use in Ghostty for unfocused surfaces.

Try clicking into and then outside of the "terminal" (yes it's upside down).
You should see a CRT-style effect that clears when you focus the terminal.

{#if blurDemoShaders.length > 0}
  <ShaderCanvas
    bind:shaders={blurDemoShaders}
    width={800}
    height={600}
    swapColorsOnClick={true}
    bind:cursorColor
    bind:prevCursorColor
    className="rounded-lg border-2 border-surface-400-600"
  />
{:else}
  <div class="flex h-[600px] w-full items-center justify-center rounded-lg border-2 border-surface-400-600 bg-surface-100-900">
    <p class="text-surface-600-400">Loading shader...</p>
  </div>
{/if}

Here I'm emulating Ghostty's `iFocus` uniform to apply a CRT shader only when the canvas is unfocused.
It may not render in every browser.
Try it directly on my site with a modern browser or zoom in really close on your phone.

## Example: Focus Animations

Here we emulate the `iTimeFocus` and `iFocus` uniforms to create a cursor highlight effect.

Click into the "terminal" and a cursor focus zoom effect triggers.
Click around more, the effect only plays once.
Defocus the "terminal" then focus it again and you'll replay the focus animation.

{#if focusDemoShaders.length > 0}
  <ShaderCanvas
    bind:shaders={focusDemoShaders}
    width={800}
    height={600}
    swapColorsOnClick={true}
    bind:cursorColor
    bind:prevCursorColor
    className="rounded-lg border-2 border-surface-400-600"
  />
{:else}
  <div class="flex h-[600px] w-full items-center justify-center rounded-lg border-2 border-surface-400-600 bg-surface-100-900">
    <p class="text-surface-600-400">Loading shader...</p>
  </div>
{/if}

If you'd like to play with these shaders more, check out my [Ghosty Shaders][shaders] demo page.

Toggle between the shaders using the menu button in the top-left of the demo canvas, or look at the debug overlay (bottom right) for uniform details.

## How it works: Focus/Blur Effects

The `iFocus` uniform is an `int` set to either `0` (blurred) or `1` (focused).
This uniform is useful for controlling whether a shader renders on focused or blurred surfaces (panes or windows).

To save resources, Ghostty shaders do not run on every frame for unfocused surface.
This is generally a good thing and saves resources, except sometimes unfocused surfaces receive frames anyway.
This can cause stutters and skips in a paused animation or cursor effect on blurred surfaces.

With `iFocus` we can intentionally apply blurred styles to unfocused surfaces or run shaders only on focused frames.
This fixes the stutters and skips by allowing the shader to ignoring these deceptive frames.

### Example Usage

The following `if` statement can be added to any shader to make it render only when blurred. You can
check `if (iFocus == 1)` to run something only when focused instead.

```glsl
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord.xy / iResolution.xy;

    // Early exit when focused
    if (iFocus == 0) {
        // Render normal texture when
        fragColor = texture(iChannel0, uv);
        return;
    }

    // The rest of the shader goes here...
}
```

This example disables all shader effects when the surface is focused, allowing unfocused states such as CRT scanlines, vignettes, shadows, or faded colors that render only on the blurred surfaces.

On the other hand, if you invert the logic you can run something only when the surface is focused,
which I have used to make my cursor effects only appear on focused surfaces.

## Focus Resume Animations

The `iTimeFocus` uniform is a `float`, set to the `iTime` when the surface last received focus.
We can use this to render effects that need to animate when the surface is focused.

Here's an example of how to use `iTimeFocus` to create a resume animation.

```glsl
const float PULSE_DURATION = 0.15;  // Animation duration in seconds

void mainImage(out vec4 fragColor, in vec2 fragCoord) {
    vec2 uv = fragCoord / iResolution.xy;

    // Quick exit: only run during active focus animation
    float timeSinceFocus = iTime - iTimeFocus;
    if (iFocus == 0 || timeSinceFocus < 0.0 || timeSinceFocus > PULSE_DURATION) {
        fragColor = texture(iChannel0, uv);
        return;
    }

    // Render focus resume animation...
}
```

In this example, the shader checks how much time has passed since the surface received focus.
If the surface is blurred, or if the focus animation duration has passed, the shader exits early to
save resources.

You can see and install my [focus-cursor][focus-cursor] shader for a complete example.

## Rendering shaders on blurred surfaces

Ghostty shaders mostly don't receive frames when blurred.
This is for performance reasons and can be disabled with the `custom-shader-animation=always` config option.

However, it's inconsistent. Sometimes defocused surfaces get frames when a mod key is press (like
command or alt) or when the mouse moves over a link.
With `iFocus` you can intentionally filter these frames out to avoid ugly stutters.

So far it seems blurred surfaces reliably receive at least one defocused frame to render the defocused state.
While you can't ensure that an animation could complete on blur (without `custom-shader-animation=always`), you can at least ensure that the defocused state is rendered once.

Therefore, I would suggest that all shaders should include this gating `if` statement unless they are intended to run `always`.
It improves performance and polish on both focused state cursor animations and blurred state rendering.

## Installing Ghostty prerelease

Again, if you want to use these shaders, you'll need to [install the prerelease][prerelease] version of Ghostty.
It might be glitchy (I immediately submitted another bug fix after this feature because the dev build had a problem)

[prerelease]: https://ghostty.org/docs/install/pre 'Docs: Install Ghostty Prerelease'
[fun-with-shaders]: https://catskull.net/fun-with-ghostty-shaders.html 'Fun with Ghostty Shaders'
[pr]: https://github.com/ghostty-org/ghostty/pull/10130 'Add iFocus and iTimeFocus shader uniforms'
[focus-cursor]: https://github.com/martinemde/dotfiles/blob/main/home/dot_config/ghostty/shaders/focus_cursor.glsl 'A shader that zoom fades the cursor on focus'
[shaders]: /shaders "Martin's Ghostty Shaders"]]></source:markdown>
		</item>
		<item>
			<title>Progress Bars in Ghostty Terminals</title>
			<description>The blue bar at the top of the Ghostty terminal can display more information than I realized.</description>
			<link>https://martinemde.com/blog/ghostty-progress-bars</link>
			<guid isPermaLink="true">https://martinemde.com/blog/ghostty-progress-bars</guid>
			<pubDate>Wed, 31 Dec 2025 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>I notice that the blue bar at the top of the Ghostty terminal can do more than just bounce back and forth (like it does in Claude Code). I was tinkering with Ghostty (to add additional shader uniforms for focus change events) and when Zig builds, it updates the Ghostty progress bar as it builds, turning red if it fails.</p>
<p>I <a href="https://github.com/ghostty-org/ghostty/pull/8477#issuecomment-3302958474">found this "one-liner"</a> (it's a <em>long</em> line) by <a href="https://github.com/jcollie">@jcollie</a> that outputs the OSC9;4 progress bar sequences:</p>
<pre><code class="language-bash"> counter=1; while [ $counter -le 25 ]; do printf "\x1b]9;4;2;${counter}\x07"; ((counter++)); sleep 0.1; done; while [ $counter -le 50 ]; do printf "\x1b]9;4;1;${counter}\x07"; ((counter++)); sleep 0.1; done; while [ $counter -le 75 ]; do printf "\x1b]9;4;3\x07"; ((counter++)); sleep 0.1; done; while [ $counter -le 100 ]; do printf "\x1b]9;4;1;${counter}\x07"; ((counter++)); sleep 0.1; done; printf "\x1b]9;4;0\x07"
</code></pre>
<p>And here's the breakdown of the escape sequence (quoting from the <a href="https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences">Windows Terminal docs of all places</a>):</p>
<pre><code class="language-sh">ESC ] 9 ; 4 ; &#x3C;state> ; &#x3C;progress> BEL
</code></pre>
<ul>
<li><code>ESC</code> is the escape character, ASCII 27.</li>
<li><code>BEL</code> is the bell character, ASCII 7.</li>
<li><code>&#x3C;state></code> is one of <code>0</code>, <code>1</code>, <code>2</code>, <code>3</code>, or <code>4</code>.
<ul>
<li><code>0</code> i s the default state, and indicates that the progress bar should be hidden. Use this state when the command is complete, to clear out any progress state.</li>
<li><code>1</code>: set progress value to <code>&#x3C;progress></code>, in the "default" state.</li>
<li><code>2</code>: set progress value to <code>&#x3C;progress></code>, in the "Error" state</li>
<li><code>3</code>: set the taskbar to the "Indeterminate" state. This is useful for commands that don't have a progress value, but are still running. This state ignores the <code>&#x3C;progress></code> value.</li>
<li><code>4</code>: set progress value to <code>&#x3C;progress></code>, in the "Warning" state</li>
</ul>
</li>
<li><code>&#x3C;progress></code> is a number between 0 and 100, inclusive.</li>
</ul>
<p>You can also refer to the ghostty documentation for <a href="https://ghostty.org/docs/vt/osc/conemu">ConEmu OSC 9;n Escape
Sequences</a></p>
<p>How great would it be to add that to other tools that compile or churn for a while? It's no replacement for other feedback since not all terminals implement this, but it seems like a good way to give extra information without an excess of printed characters that get picked up by logs.</p>
]]></content:encoded>
			<source:markdown><![CDATA[I notice that the blue bar at the top of the Ghostty terminal can do more than just bounce back and forth (like it does in Claude Code). I was tinkering with Ghostty (to add additional shader uniforms for focus change events) and when Zig builds, it updates the Ghostty progress bar as it builds, turning red if it fails.

I [found this "one-liner"](https://github.com/ghostty-org/ghostty/pull/8477#issuecomment-3302958474) (it's a _long_ line) by [@jcollie](https://github.com/jcollie) that outputs the OSC9;4 progress bar sequences:

```bash
 counter=1; while [ $counter -le 25 ]; do printf "\x1b]9;4;2;${counter}\x07"; ((counter++)); sleep 0.1; done; while [ $counter -le 50 ]; do printf "\x1b]9;4;1;${counter}\x07"; ((counter++)); sleep 0.1; done; while [ $counter -le 75 ]; do printf "\x1b]9;4;3\x07"; ((counter++)); sleep 0.1; done; while [ $counter -le 100 ]; do printf "\x1b]9;4;1;${counter}\x07"; ((counter++)); sleep 0.1; done; printf "\x1b]9;4;0\x07"
```

And here's the breakdown of the escape sequence (quoting from the [Windows Terminal docs of all places](https://learn.microsoft.com/en-us/windows/terminal/tutorials/progress-bar-sequences)):

```sh
ESC ] 9 ; 4 ; <state> ; <progress> BEL
```

- `ESC` is the escape character, ASCII 27.
- `BEL` is the bell character, ASCII 7.
- `<state>` is one of `0`, `1`, `2`, `3`, or `4`.
  - `0` i s the default state, and indicates that the progress bar should be hidden. Use this state when the command is complete, to clear out any progress state.
  - `1`: set progress value to `<progress>`, in the "default" state.
  - `2`: set progress value to `<progress>`, in the "Error" state
  - `3`: set the taskbar to the "Indeterminate" state. This is useful for commands that don't have a progress value, but are still running. This state ignores the `<progress>` value.
  - `4`: set progress value to `<progress>`, in the "Warning" state
- `<progress>` is a number between 0 and 100, inclusive.

You can also refer to the ghostty documentation for [ConEmu OSC 9;n Escape
Sequences](https://ghostty.org/docs/vt/osc/conemu)

How great would it be to add that to other tools that compile or churn for a while? It's no replacement for other feedback since not all terminals implement this, but it seems like a good way to give extra information without an excess of printed characters that get picked up by logs.]]></source:markdown>
		</item>
		<item>
			<title>Show Your Work: How to write reviewable code</title>
			<description>As authors of code, we&apos;re responsible for writing reviewable code. When agents write the code, you still need to show your work, even if that work looks different now.</description>
			<link>https://martinemde.com/blog/show-your-work</link>
			<guid isPermaLink="true">https://martinemde.com/blog/show-your-work</guid>
			<pubDate>Fri, 19 Dec 2025 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>I still remember a code review I received early in my career. I had just started a new job and my new coworker taught me something in a code review I haven't forgotten. I think the lesson applies more than ever now that agents are writing more (most?) of our code.</p>
<p>Having freshly joined, I made an commit to address excess white space and improper indentation that were bugging me (painstakingly, by hand, it was the 2000s) and along the way I made a few refactors that seemed obvious to me. I committed this all with the message "White space fixes" and submitted for review.</p>
<p>My colleague quickly called me out. "You said these were just white space fixes, why did this code here change?" My explanation was something about not wasting the overhead of splitting it out. "If I'm scanning for white space changes, I don't want to suddenly have to review for code correctness." OK. I reset my commit, added back only the white space changes, and resubmitted them each separately. Both were commits accepted easily.</p>
<p>However, the lesson stuck with me. Even though the code was fine, I had made it much harder for him to review. White space changes are easy to scroll through. Your only job is to catch accidental code changes, which is exactly what I inserted. I had broken his expectation about how long this code review would take, what level of care he needed to use, and how big an interruption this would be to his workflow.</p>
<p><strong>No one has to merge your code. They got shit to do.</strong></p>
<p>The burden is on the author to submit code that can be reviewed easily. This is an often unspoken part of the senior engineer skill set, <a href="https://obie.medium.com/what-happens-when-the-coding-becomes-the-least-interesting-part-of-the-work-ab10c213c660" title="Teams rarely standardize or document senior thinking">transferred by working together</a> rather than explicitly taught. Before you learn this skill, you'll sometimes notice than your larger commits and clever refactors take a long time to get reviewed. You may even think this is the fault of your team or your process. Maybe it is, or maybe the whole team assumes that code review always takes forever so they should do it in big batches.</p>
<p>The requirement to make your code reviewable does not change when agents write your code. Your responsibility for making your code reviewable is more important than ever. And what makes code reviewable? Put simply, <strong>show your work!</strong></p>
<p>Before agents, your work was a stream of reasonably concise commits, comments, tests, and code, submitted as incremental updates. Now, while all of the old stuff still matters, your work includes the specs, prompts, decisions, and references that you used to supply context to your agent. Combined these artifacts <a href="https://simonwillison.net/2025/Dec/18/code-proven-to-work/">prove</a> that your change does what you claim it does.</p>
<p>When a reviewer tries to understand the code, they need to assess whether you effectively achieved your goal. The larger your change the harder it is to understand all the various goals you had while making it. As the author, it's also much harder to prove, since there's so much more code to justify.</p>
<p>Share your <code>PLAN.md</code> early and seek feedback. <strong>I'd much rather critique 2 pages of <code>PLAN.md</code> rather than 20 pages of code.</strong> You'll have a easier time generating code that can be reviewed effectively when someone has already seen what you're trying to achieve.</p>
<p>Reviewing the plan, and including it with your code for review, also give the reviewer a chance to see your words and approach, and to offer real feedback about the work you’re actually doing. Resist the urge to add more than one plan to a unit of reviewable code. If you want to keep building after completing a plan, wait! <a href="https://martinemde.com/blog/its-just-their-tokens" title="Reviewing AI agent produced code should change how we code review">Go review someone else's code</a> first, then develop your next plan.</p>
<p>Prove that you did what you set out to do, show it clearly in your code, tests, and commits. Separate refactors from features and cleanup. If something you find out you need to do after can be done before, reorder the commits. Break code into smaller chunks to make them easier to review, build them in layers of abstraction and keep changes focused.</p>
]]></content:encoded>
			<source:markdown><![CDATA[I still remember a code review I received early in my career. I had just started a new job and my new coworker taught me something in a code review I haven't forgotten. I think the lesson applies more than ever now that agents are writing more (most?) of our code.

Having freshly joined, I made an commit to address excess white space and improper indentation that were bugging me (painstakingly, by hand, it was the 2000s) and along the way I made a few refactors that seemed obvious to me. I committed this all with the message "White space fixes" and submitted for review.

My colleague quickly called me out. "You said these were just white space fixes, why did this code here change?" My explanation was something about not wasting the overhead of splitting it out. "If I'm scanning for white space changes, I don't want to suddenly have to review for code correctness." OK. I reset my commit, added back only the white space changes, and resubmitted them each separately. Both were commits accepted easily.

However, the lesson stuck with me. Even though the code was fine, I had made it much harder for him to review. White space changes are easy to scroll through. Your only job is to catch accidental code changes, which is exactly what I inserted. I had broken his expectation about how long this code review would take, what level of care he needed to use, and how big an interruption this would be to his workflow.

**No one has to merge your code. They got shit to do.**

The burden is on the author to submit code that can be reviewed easily. This is an often unspoken part of the senior engineer skill set, [transferred by working together][senior] rather than explicitly taught. Before you learn this skill, you'll sometimes notice than your larger commits and clever refactors take a long time to get reviewed. You may even think this is the fault of your team or your process. Maybe it is, or maybe the whole team assumes that code review always takes forever so they should do it in big batches.

The requirement to make your code reviewable does not change when agents write your code. Your responsibility for making your code reviewable is more important than ever. And what makes code reviewable? Put simply, **show your work!**

Before agents, your work was a stream of reasonably concise commits, comments, tests, and code, submitted as incremental updates. Now, while all of the old stuff still matters, your work includes the specs, prompts, decisions, and references that you used to supply context to your agent. Combined these artifacts [prove][prove] that your change does what you claim it does.

When a reviewer tries to understand the code, they need to assess whether you effectively achieved your goal. The larger your change the harder it is to understand all the various goals you had while making it. As the author, it's also much harder to prove, since there's so much more code to justify.

Share your `PLAN.md` early and seek feedback. **I'd much rather critique 2 pages of `PLAN.md` rather than 20 pages of code.** You'll have a easier time generating code that can be reviewed effectively when someone has already seen what you're trying to achieve.

Reviewing the plan, and including it with your code for review, also give the reviewer a chance to see your words and approach, and to offer real feedback about the work you’re actually doing. Resist the urge to add more than one plan to a unit of reviewable code. If you want to keep building after completing a plan, wait! [Go review someone else's code][review] first, then develop your next plan.

Prove that you did what you set out to do, show it clearly in your code, tests, and commits. Separate refactors from features and cleanup. If something you find out you need to do after can be done before, reorder the commits. Break code into smaller chunks to make them easier to review, build them in layers of abstraction and keep changes focused.

[senior]: https://obie.medium.com/what-happens-when-the-coding-becomes-the-least-interesting-part-of-the-work-ab10c213c660 'Teams rarely standardize or document senior thinking'
[review]: /blog/its-just-their-tokens 'Reviewing AI agent produced code should change how we code review'
[prove]: https://simonwillison.net/2025/Dec/18/code-proven-to-work/]]></source:markdown>
		</item>
		<item>
			<title>It&apos;s Just Their Tokens: Code Review Etiquette in the Vibe Era</title>
			<description>Code review has long been a bottle neck and agents are making it worse. How can we address the increasing demand for review without destroying code review or creating new bottlenecks?</description>
			<link>https://martinemde.com/blog/its-just-their-tokens</link>
			<guid isPermaLink="true">https://martinemde.com/blog/its-just-their-tokens</guid>
			<pubDate>Thu, 18 Dec 2025 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>A common complaint about the usage of AI coding agents is the burden it places on code reviewers. Engineers often resist switching their mental context to review code. Finding out that you're now expected to review someone else's low-effort slop can almost feel like an insult.</p>
<p>Code review has long been a bottle neck and agents are making it worse. More code, produced faster and with less oversight increases the burden on reviewers. And yet, code reviews are an essential part of sharing knowledge in software engineering. They follow an etiquette that is meant to respect the effort of the author and encourage sharing of knowledge.</p>
<p>Meanwhile, the expectations placed on the code reviewer, and the bleak future where all we do is review the output of coding agents, feels impossibly unbalanced. Much of this etiquette, and the expectations placed on the reviewer, assumes that writing code is slow and hard. This poses a problem if we want engineers to be effective with AI agents. We need code review to teach, learn, and ensure quality.</p>
<p>How can we address the increasing demand for review without destroying code review or creating new bottlenecks?</p>
<h2>Review Etiquette Must Change</h2>
<p>The length of a PR is no longer a good predictor of effort. If you're staring down an angry bowl of vibe soup, <strong>you'll do the author a service by explaining the words they need to say to their agent to produce the quality you're expecting.</strong></p>
<p>Normally reviewers might shy away from asking for a big architectural change in code reviews. Don't. Big architecture changes are not as difficult as they once were and vibe code can be re-vibed easily with better requirements. <strong>Reviewers should reject sloppy code as long as they make their expectations clear.</strong></p>
<p>How do we balance the effort to review the code with the effort to generate the code? My advice, keep the effort proportional. If a day of work in the before times took 20 minutes to review, then an hour of work should take only a few minutes. Allowing that hour to take 20 minutes threatens the balance of work between author and reviewer. This means the burden of quality, and the obligation to prove that quality, must remain on the author.</p>
<p>If the author submitted a big mess, ask for a big solution. If the slop is high, simply scan to understand their goal and then respond with the architecture or solution you expected and why. Leave high level comments for code that lacks deep consideration. Ask questions that help you both understand the problem and the possible solutions. Focus on what would make their code easier to review.</p>
<p>This isn't a blank check to be a jerk. Kindness and respect are still table stakes. If something doesn't meet your quality standards, nitpicking every problem line-by-line is counterproductive. Be direct about what you expect overall, ask questions about their goals, and learn how they arrived at the solution. Your input may help improve the quality and readability of the author's future code and maybe you'll even learn something yourself.</p>
<p>If an author drops a giant review on your lap without warning, vibe coded or not, don't be afraid push back. Careful code review is limited resource. It’s OK to expect the author to make code review easier. You're not imposing on the author. This isn't their blood, sweat, and tears, it's just their tokens.</p>
<hr>
<p><em>My first draft of this blog post was about 4x longer and half as good. Thanks to my teammate Denis and my “infinitely patient with me wife” Kewe for their reviews. You both helped me merge a better blog post.</em></p>
]]></content:encoded>
			<source:markdown><![CDATA[A common complaint about the usage of AI coding agents is the burden it places on code reviewers. Engineers often resist switching their mental context to review code. Finding out that you're now expected to review someone else's low-effort slop can almost feel like an insult.

Code review has long been a bottle neck and agents are making it worse. More code, produced faster and with less oversight increases the burden on reviewers. And yet, code reviews are an essential part of sharing knowledge in software engineering. They follow an etiquette that is meant to respect the effort of the author and encourage sharing of knowledge.

Meanwhile, the expectations placed on the code reviewer, and the bleak future where all we do is review the output of coding agents, feels impossibly unbalanced. Much of this etiquette, and the expectations placed on the reviewer, assumes that writing code is slow and hard. This poses a problem if we want engineers to be effective with AI agents. We need code review to teach, learn, and ensure quality.

How can we address the increasing demand for review without destroying code review or creating new bottlenecks?

## Review Etiquette Must Change

The length of a PR is no longer a good predictor of effort. If you're staring down an angry bowl of vibe soup, **you'll do the author a service by explaining the words they need to say to their agent to produce the quality you're expecting.**

Normally reviewers might shy away from asking for a big architectural change in code reviews. Don't. Big architecture changes are not as difficult as they once were and vibe code can be re-vibed easily with better requirements. **Reviewers should reject sloppy code as long as they make their expectations clear.**

How do we balance the effort to review the code with the effort to generate the code? My advice, keep the effort proportional. If a day of work in the before times took 20 minutes to review, then an hour of work should take only a few minutes. Allowing that hour to take 20 minutes threatens the balance of work between author and reviewer. This means the burden of quality, and the obligation to prove that quality, must remain on the author.

If the author submitted a big mess, ask for a big solution. If the slop is high, simply scan to understand their goal and then respond with the architecture or solution you expected and why. Leave high level comments for code that lacks deep consideration. Ask questions that help you both understand the problem and the possible solutions. Focus on what would make their code easier to review.

This isn't a blank check to be a jerk. Kindness and respect are still table stakes. If something doesn't meet your quality standards, nitpicking every problem line-by-line is counterproductive. Be direct about what you expect overall, ask questions about their goals, and learn how they arrived at the solution. Your input may help improve the quality and readability of the author's future code and maybe you'll even learn something yourself.

If an author drops a giant review on your lap without warning, vibe coded or not, don't be afraid push back. Careful code review is limited resource. It’s OK to expect the author to make code review easier. You're not imposing on the author. This isn't their blood, sweat, and tears, it's just their tokens.

---

_My first draft of this blog post was about 4x longer and half as good. Thanks to my teammate Denis and my “infinitely patient with me wife” Kewe for their reviews. You both helped me merge a better blog post._]]></source:markdown>
		</item>
		<item>
			<title>Adapting Engineering Orgs for Non-Technical Coders</title>
			<description>These are the action items for engineering organizations that want to empower the new vibe coding juniors</description>
			<link>https://martinemde.com/blog/adapt-engineering-orgs-to-non-technical-coders</link>
			<guid isPermaLink="true">https://martinemde.com/blog/adapt-engineering-orgs-to-non-technical-coders</guid>
			<pubDate>Sun, 14 Dec 2025 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>Vibe coding is allowing a new category of junior programmers to emerge from unlikely places. Engineering is going to change.
Here's the <code>TODO</code> items that I think will help address the shift that I see happening.</p>
<p>As I addressed in my previous article,
<a href="https://martinemde.com/blog/hire-junior-engineers-now">in the era of Opus vibe coding, junior engineers aren't useless</a>.
We need them to fill in the gap between domain experts and senior engineers so that we can realize
the efficiency gains that we want to see from coding agents.</p>
<p>I've extracted the TODO list here since the previous article got very long. As per the article, I'm
using "domain expert" here to indicate someone that has years of experience in your company but
little to no experience writing code. Many of these experts are excited about fixing their own
problems, and this motivation is special and valuable. Let's empower them.</p>
<h2>Technical TODOs</h2>
<ol>
<li>Template projects that encode best practices and security controls from the start, these are your blessed paths that every new Cursor project should start with.</li>
<li>Push-button deployment paths that work safely for non-engineers. Make it one click or it's too complicated. Look at <a href="https://www.fastly.com/documentation/solutions/starters/compute-starter-kit-go-default/" title="Fastly 1-click deployment starter kit">Fastly</a>, <a href="https://github.com/cloudflare/workflows-starter" title="Cloudflare work starter apps with 1 click deploys">Cloudflare</a>, or <a href="https://devcenter.heroku.com/start" title="Heroku quick start guide">Heroku</a> to see how they approach push-button deployment. They're surprisingly easy to get started despite targeting engineers.</li>
<li>Agent rules and context that guide AI coding agents toward your company's blessed paths before anyone writes a line of code.</li>
<li>Guardrails that catch sensitive data access and security issues automatically.</li>
<li>Code review agents that help onboard people into safe practices and explain what they're catching and why.</li>
<li>Tiered GitHub organization structures that give power to the right people without locking people out. Access to GitHub is no longer an indicator of good engineering judgement. Not everyone needs the <em>same</em> access to GitHub, but everyone needs a safe place to share code.</li>
</ol>
<h2>Organizational TODOs</h2>
<ol>
<li>Create a reach-out team to find the vibe-newbs in your organization who are already using AI tools to code. They're already there, in operations, in customer support, in finance. Find them before they create shadow IT problems.</li>
<li>Hire supportive junior engineers who are familiar with AI-assisted coding. Hire them explicitly with the mandate to teach. Value tutoring or mentoring experience.</li>
<li>Pair these early-career engineers with domain experts who are vibe coding. Their primary job is to support the domain experts to contribute and bridge the gap with platform engineers.</li>
<li>Senior engineering talent should guide and support these entry-level engineers. As you build the platform, grow your early career engineers to contribute to the platform.</li>
<li>Invest heavily in your platform. Are you ready for non-engineers to deploy new applications?</li>
<li>Investments in better bootstrapping, templates, context, and blessed paths support ALL engineers, not just the most junior. The investments we made in generators have already paid off, but be prepared to support them.</li>
<li>A repeat of the above, but go look at how <a href="https://www.fastly.com/documentation/solutions/starters/compute-starter-kit-go-default/" title="Fastly 1-click deployment starter kit">Fastly</a>, <a href="https://github.com/cloudflare/workflows-starter" title="Cloudflare work starter apps with 1 click deploys">Cloudflare</a>, and <a href="https://devcenter.heroku.com/start" title="Heroku quick start guide">Heroku</a> provide templates for new projects with push-button deploys. You need to create your company's preferred templates and push-button deploys that reinforce your company's best practices. AI agents will build to the structures you give them.</li>
</ol>
<p>The domain experts turning to vibe coding are showing that they're up for a challenge and they're willing to solve problems that no one has solved for them yet. This of the inside of your company as a microcosm of the outside world. The bigger your company is, the more you need to create on-ramps for people to grow into stronger contributors.</p>
<p>If <a href="https://www.fastly.com/blog/senior-developers-ship-more-ai-code" title="survey of 791 developers found a notable difference in how much AI-generated code is making it into production">senior engineers ship 2.5x more AI code than juniors</a> and <a href="https://www.sonarworks.com/blog/research/ai-music-production-2025" title="1 in 4 producers now use AI tools in their workflow">AI Savy music producers outperform</a> their peers, then how might your most experienced marketers, corporate financiers, or business developer perform if they're supported with the tools and team mates they need?</p>
]]></content:encoded>
			<source:markdown><![CDATA[Vibe coding is allowing a new category of junior programmers to emerge from unlikely places. Engineering is going to change.
Here's the `TODO` items that I think will help address the shift that I see happening.

As I addressed in my previous article,
[in the era of Opus vibe coding, junior engineers aren't useless](/blog/hire-junior-engineers-now).
We need them to fill in the gap between domain experts and senior engineers so that we can realize
the efficiency gains that we want to see from coding agents.

I've extracted the TODO list here since the previous article got very long. As per the article, I'm
using "domain expert" here to indicate someone that has years of experience in your company but
little to no experience writing code. Many of these experts are excited about fixing their own
problems, and this motivation is special and valuable. Let's empower them.

## Technical TODOs

1. Template projects that encode best practices and security controls from the start, these are your blessed paths that every new Cursor project should start with.
2. Push-button deployment paths that work safely for non-engineers. Make it one click or it's too complicated. Look at [Fastly][1], [Cloudflare][2], or [Heroku][3] to see how they approach push-button deployment. They're surprisingly easy to get started despite targeting engineers.
3. Agent rules and context that guide AI coding agents toward your company's blessed paths before anyone writes a line of code.
4. Guardrails that catch sensitive data access and security issues automatically.
5. Code review agents that help onboard people into safe practices and explain what they're catching and why.
6. Tiered GitHub organization structures that give power to the right people without locking people out. Access to GitHub is no longer an indicator of good engineering judgement. Not everyone needs the _same_ access to GitHub, but everyone needs a safe place to share code.

## Organizational TODOs

1. Create a reach-out team to find the vibe-newbs in your organization who are already using AI tools to code. They're already there, in operations, in customer support, in finance. Find them before they create shadow IT problems.
2. Hire supportive junior engineers who are familiar with AI-assisted coding. Hire them explicitly with the mandate to teach. Value tutoring or mentoring experience.
3. Pair these early-career engineers with domain experts who are vibe coding. Their primary job is to support the domain experts to contribute and bridge the gap with platform engineers.
4. Senior engineering talent should guide and support these entry-level engineers. As you build the platform, grow your early career engineers to contribute to the platform.
5. Invest heavily in your platform. Are you ready for non-engineers to deploy new applications?
6. Investments in better bootstrapping, templates, context, and blessed paths support ALL engineers, not just the most junior. The investments we made in generators have already paid off, but be prepared to support them.
7. A repeat of the above, but go look at how [Fastly][1], [Cloudflare][2], and [Heroku][3] provide templates for new projects with push-button deploys. You need to create your company's preferred templates and push-button deploys that reinforce your company's best practices. AI agents will build to the structures you give them.

The domain experts turning to vibe coding are showing that they're up for a challenge and they're willing to solve problems that no one has solved for them yet. This of the inside of your company as a microcosm of the outside world. The bigger your company is, the more you need to create on-ramps for people to grow into stronger contributors.

If [senior engineers ship 2.5x more AI code than juniors][4] and [AI Savy music producers outperform][5] their peers, then how might your most experienced marketers, corporate financiers, or business developer perform if they're supported with the tools and team mates they need?

[1]: https://www.fastly.com/documentation/solutions/starters/compute-starter-kit-go-default/ 'Fastly 1-click deployment starter kit'
[2]: https://github.com/cloudflare/workflows-starter 'Cloudflare work starter apps with 1 click deploys'
[3]: https://devcenter.heroku.com/start 'Heroku quick start guide'
[4]: https://www.fastly.com/blog/senior-developers-ship-more-ai-code 'survey of 791 developers found a notable difference in how much AI-generated code is making it into production'
[5]: https://www.sonarworks.com/blog/research/ai-music-production-2025 '1 in 4 producers now use AI tools in their workflow']]></source:markdown>
		</item>
		<item>
			<title>Hire Junior Engineers Now</title>
			<description>AI coding tools are creating a larger gap between the most junior and senior engineers: we need early career programmers to fill the gap</description>
			<link>https://martinemde.com/blog/hire-junior-engineers-now</link>
			<guid isPermaLink="true">https://martinemde.com/blog/hire-junior-engineers-now</guid>
			<pubDate>Tue, 28 Oct 2025 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>Vibe coding is allowing a new category of junior programmers to emerge from unlikely places.</p>
<p>When a vibe coder joins an engineering channel and asks a simple question: "How do I get a domain name so I can share this with my team?" it's all too easy to assume a level of engineering know-how that isn't yet developed.</p>
<p>When this happened at my work, I joined a Zoom meeting to try to help. What I saw both inspired me and shifted my view about the role of engineering in larger organizations.</p>
<p>Most people think that the massive shift that's coming will put junior engineers out of a job. I disagree.</p>
<p>We're facing a massive shift in how engineering teams need to work, and if you think we'll just fire
all the juniors and then sit around guiding swarms of coding agents, you're going to be surprised.</p>
<h2>The Gap Between Vibes and Infrastructure</h2>
<p>The traditional engineering answer to "how do I share this?" goes something like: "Work with platform ops to get a deployment set up. Make a PR to the terraform to allocate a new internal subdomain. If you're deploying a docker container, can you configure an ECR repo too? The app generator should help you do this if it follows our standard framework guidelines."</p>
<p>To a Customer Service manager or Accountant who just vibed out a solution to their problem, this is complete gibberish.</p>
<p>If they're persistent, they'll ask again. They'll probably feel discouraged. Maybe they'll figure it out themselves in ways that would make security teams blush.</p>
<p>This creates problems no company wants:</p>
<ul>
<li>Domain experts, people with deep experience in your company, get discouraged from contributing to
AI driven solutions</li>
<li><a href="https://en.wikipedia.org/wiki/Shadow_IT">Shadow IT</a> starts cropping up everywhere, allowing customer data, financial information, or company IP to leak outside of IT controls</li>
<li>Senior engineers spending days supporting small contributions from inexperienced vibe coders, erasing the productivity promise of AI</li>
<li>Security vulnerabilities created by well-meaning people who don't know any better</li>
</ul>
<p>The technical solutions that might work for an engineer could be dangerous for inexperienced vibe-coders. The gap is huge between a working prototype in cursor and a safe deployment.</p>
<h2>Vibe Coding</h2>
<p>As an experienced engineer, I see huge productivity gains from using AI coding tools, but they're not without their problems.</p>
<p>Engineers can see when our coding agents are going sideways. When Claude starts dropping a new <code>ALLCAPS.md</code> for every prompt, when it create the 3rd copy of a script it already wrote, we know it's time to reign it in and adjust rules or clarify requirements. When Claude does something stupid, we reset back to the last commit.</p>
<p>New vibe coders don't know any of this. They don't use git. They don't reset context. They use one conversation in cursor for the entire program they're writing.</p>
<p>Junior vibe coders end up with a directory containing a mix of sensitive data, cross-linked python files, 20 random scripts for one off changes, and 50 variations of <code>SIMPLE_QUICK_STARTUP_README_FIXED.md</code>. Worse still, they do not know which files might contain PII or financial data. Working with sensitive data is their job. The concept of sanitized test data is foreign to them.</p>
<p>Vibe Coding is its own skill set. It's an offshoot of engineering that benefits from the skills we've built as engineers. Knowing what a good application looks like help you build a good application.</p>
<p>After helping my son and my wife get started with Cursor, I can see how sharing my <a href="https://obie.medium.com/what-happens-when-the-coding-becomes-the-least-interesting-part-of-the-work-ab10c213c660">senior thinking</a> with them as they learn changes the sorts of outcomes they get.</p>
<blockquote>
<p>If I’m working alone, it all stays internal. If I’m pairing with someone else that’s senior, then the thinking surfaces naturally in conversation but is taken for granted. If I’m pairing with someone less experienced, I might slow down enough to explain parts of my senior thinking, but definitely not all. It would bog us down too much.</p>
<ul>
<li>Obie Fernandez</li>
</ul>
</blockquote>
<p>Life-long engineers, ones that grew up with an engineer parent or were introduced to abstract
systems early, have this deeply engrained in their approach to life. When things go wrong or get
difficult for vibe coders, if they don't have any of this to fall back on, they end up with very
different results than experienced engineers.</p>
<p>It used to be that this rite of passage, understanding the system, also meant that once you got access to <code>rm -rf</code> you had enough background to know it was dangerous. Now we're facing a shift in who is capable of what, and we're going to need to decide where we want to go.</p>
<h2>We've Been Here Before</h2>
<p>Engineers faced a similar crossroads years ago. Dan McKinley captured it perfectly in his <a href="https://egoless.engineering/">"Egoless Engineering"</a> talk.</p>
<p>He describes a designer who was encouraged to contribute to an application. When the designer accidentally broke the build late at night, the team had a choice: lock it down so only "real engineers" could deploy, or make deploys safe for everyone.</p>
<p>McKinley chose the path of empowerment: "Randy is in the deploy group now. The how and when can be worked out off-list, but it is now <em>possible</em> for him to deploy to prod."</p>
<p>That decision required a mindset shift. Engineers had to ensure deploys were safe for <em>anyone</em> to run. They had to build systems that enabled contribution instead of building gates that prevented it.</p>
<p>This empowerment is what unlocked what we might call modern DevOps.</p>
<p>Now we're doing it again, but the "deploy group" is much larger than it was before and contains
a much more junior set of people.</p>
<h2>The Skill Spectrum</h2>
<p>Traditionally, a "junior engineer" meant someone fresh from college or a programming bootcamp. They were people that proved they could program <em>before</em> getting hired. They could make commits, install dependencies, and maybe run simple deployments. They knew the lingo. They could speak like an engineer.</p>
<p>Now the code skill spectrum is doubling. A new "expert junior" tier has been added. They know a ton about the business domain, but they know next-to-nothing about engineering. There may be as many people in this new group as our entire existing engineering organization.</p>
<h2>The New Engineering</h2>
<p>Some of the best junior hires were teachers, healthcare workers, or business analysts who brought 10+ years of domain expertise along with their junior engineering skill set. They had <em>senior thinking</em> from a different domain, and these executive skills somewhat compensated for their lack of engineering knowledge.</p>
<p>Domain Expert Vibe Coders (the new "expert juniors") bring deep domain knowledge, and now that they come from within our businesses, they have direct useful experience. They can build prototypes with AI assistance that solve very specific problems that impact only their jobs. Stuff they would struggle to ask an engineer to help them with (if they were lucky enough to have an engineer contact in the company).</p>
<p>We used to consider someone junior when they were a <em>Classically Trained Entry-Level Engineer</em>. They have 4 year CS degrees or equivalent experience. They understood coding enough to convince a series of instructors, employers and peers.</p>
<p>These entry-level engineers are some of the people that AI Doomers and AGI-Optimists are trying to convince us that we won't need anymore. The doom and gloom story says that juniors are useless now because a senior has a fleet of juniors to do their work. People (rightly) worry how we will train the next generation of engineers if the engineering org is just senior talent now.</p>
<h2>The Full-Stack Team</h2>
<p>Engineering needs to spread out across the entire company. We need to redefine the full-stack team
to span from accounting to engineering. Including a designer and a product manager on our team is no
longer "full-stack" enough. We need a gradient of engineers embedded in every part of our company.</p>
<p>To get the most of their expertise, we can pair domain experts with entry-level engineers. You get domain expertise (accounting, legal, HR, operations) combined with the growing system thinker with the engineering vocabulary. The entry-level engineer provides a bridge from accounting speak into infrastructure speak. They help the Senior Engineers focus on capital-E engineering.</p>
<p>Senior Engineers become platform builders. In the before times, senior engineers would work with
product managers and designers to understand the domain. Now we need seniors to focus on the hard
stuff. Engineers are domain experts in engineering. They need to create the systems that make
safe self-service possible. They need to support the entry-level and junior experts, help them
level up, and give them the tools to contribute effectively.</p>
<p>This gradient of engineer skill, from viber to junior to senior, improves the efficiency of
each conversation while allowing work to continue at the right level of abstraction, where each
person is most skilled.</p>
<h2>What This Means for Hiring</h2>
<p>Now the shift begins: Domain experts are the new junior engineering talent. Entry-level
engineers fresh out of college are the mid level talent. They have a natural progression
to senior while filling a need in the company that keeps seniors effective and productive.</p>
<p>It would be a huge mistake to stop hiring early career "junior" programmers. They're comparatively
less junior than the domain experts that just picked up cursor. A 4-year CS degree puts them
squarely in the middle of the pack, now that the spectrum has doubled.</p>
<p>If we fail to have these juniors at hand, we'll end up spending senior engineering time to prevent
vibe coders from leaking company documents or creating headaches. Worse yet, since engineering
management is afraid to waste their team's time, it's often the managers that deal with these new
vibe coders. The cost of deploying a quick script that generates a power point presentation, when
supported by department lead engineering managers and a platform ops team, is astronomical. This is
not what coding agents were supposed to do.</p>
<p>Get this right, and we unlock an enormous amount of domain expertise that was previously bottlenecked behind engineering teams. Get it wrong, and we create security nightmares, devalue or discourage talent, or destroy efficiency by applying senior engineering time ineffectively.</p>
<p>The technical problems are solvable. We've done this before with the DevOps movement. We can do it again with vibe coding domain experts. It requires acknowledging that the spectrum of "engineer" is now much broader than it used to be and building our teams accordingly.</p>
]]></content:encoded>
			<source:markdown><![CDATA[Vibe coding is allowing a new category of junior programmers to emerge from unlikely places.

When a vibe coder joins an engineering channel and asks a simple question: "How do I get a domain name so I can share this with my team?" it's all too easy to assume a level of engineering know-how that isn't yet developed.

When this happened at my work, I joined a Zoom meeting to try to help. What I saw both inspired me and shifted my view about the role of engineering in larger organizations.

Most people think that the massive shift that's coming will put junior engineers out of a job. I disagree.

We're facing a massive shift in how engineering teams need to work, and if you think we'll just fire
all the juniors and then sit around guiding swarms of coding agents, you're going to be surprised.

## The Gap Between Vibes and Infrastructure

The traditional engineering answer to "how do I share this?" goes something like: "Work with platform ops to get a deployment set up. Make a PR to the terraform to allocate a new internal subdomain. If you're deploying a docker container, can you configure an ECR repo too? The app generator should help you do this if it follows our standard framework guidelines."

To a Customer Service manager or Accountant who just vibed out a solution to their problem, this is complete gibberish.

If they're persistent, they'll ask again. They'll probably feel discouraged. Maybe they'll figure it out themselves in ways that would make security teams blush.

This creates problems no company wants:

- Domain experts, people with deep experience in your company, get discouraged from contributing to
  AI driven solutions
- [Shadow IT](https://en.wikipedia.org/wiki/Shadow_IT) starts cropping up everywhere, allowing customer data, financial information, or company IP to leak outside of IT controls
- Senior engineers spending days supporting small contributions from inexperienced vibe coders, erasing the productivity promise of AI
- Security vulnerabilities created by well-meaning people who don't know any better

The technical solutions that might work for an engineer could be dangerous for inexperienced vibe-coders. The gap is huge between a working prototype in cursor and a safe deployment.

## Vibe Coding

As an experienced engineer, I see huge productivity gains from using AI coding tools, but they're not without their problems.

Engineers can see when our coding agents are going sideways. When Claude starts dropping a new `ALLCAPS.md` for every prompt, when it create the 3rd copy of a script it already wrote, we know it's time to reign it in and adjust rules or clarify requirements. When Claude does something stupid, we reset back to the last commit.

New vibe coders don't know any of this. They don't use git. They don't reset context. They use one conversation in cursor for the entire program they're writing.

Junior vibe coders end up with a directory containing a mix of sensitive data, cross-linked python files, 20 random scripts for one off changes, and 50 variations of `SIMPLE_QUICK_STARTUP_README_FIXED.md`. Worse still, they do not know which files might contain PII or financial data. Working with sensitive data is their job. The concept of sanitized test data is foreign to them.

Vibe Coding is its own skill set. It's an offshoot of engineering that benefits from the skills we've built as engineers. Knowing what a good application looks like help you build a good application.

After helping my son and my wife get started with Cursor, I can see how sharing my [senior thinking](https://obie.medium.com/what-happens-when-the-coding-becomes-the-least-interesting-part-of-the-work-ab10c213c660) with them as they learn changes the sorts of outcomes they get.

> If I’m working alone, it all stays internal. If I’m pairing with someone else that’s senior, then the thinking surfaces naturally in conversation but is taken for granted. If I’m pairing with someone less experienced, I might slow down enough to explain parts of my senior thinking, but definitely not all. It would bog us down too much.
>
> - Obie Fernandez

Life-long engineers, ones that grew up with an engineer parent or were introduced to abstract
systems early, have this deeply engrained in their approach to life. When things go wrong or get
difficult for vibe coders, if they don't have any of this to fall back on, they end up with very
different results than experienced engineers.

It used to be that this rite of passage, understanding the system, also meant that once you got access to `rm -rf` you had enough background to know it was dangerous. Now we're facing a shift in who is capable of what, and we're going to need to decide where we want to go.

## We've Been Here Before

Engineers faced a similar crossroads years ago. Dan McKinley captured it perfectly in his ["Egoless Engineering"](https://egoless.engineering/) talk.

He describes a designer who was encouraged to contribute to an application. When the designer accidentally broke the build late at night, the team had a choice: lock it down so only "real engineers" could deploy, or make deploys safe for everyone.

McKinley chose the path of empowerment: "Randy is in the deploy group now. The how and when can be worked out off-list, but it is now _possible_ for him to deploy to prod."

That decision required a mindset shift. Engineers had to ensure deploys were safe for _anyone_ to run. They had to build systems that enabled contribution instead of building gates that prevented it.

This empowerment is what unlocked what we might call modern DevOps.

Now we're doing it again, but the "deploy group" is much larger than it was before and contains
a much more junior set of people.

## The Skill Spectrum

Traditionally, a "junior engineer" meant someone fresh from college or a programming bootcamp. They were people that proved they could program _before_ getting hired. They could make commits, install dependencies, and maybe run simple deployments. They knew the lingo. They could speak like an engineer.

Now the code skill spectrum is doubling. A new "expert junior" tier has been added. They know a ton about the business domain, but they know next-to-nothing about engineering. There may be as many people in this new group as our entire existing engineering organization.

## The New Engineering

Some of the best junior hires were teachers, healthcare workers, or business analysts who brought 10+ years of domain expertise along with their junior engineering skill set. They had _senior thinking_ from a different domain, and these executive skills somewhat compensated for their lack of engineering knowledge.

Domain Expert Vibe Coders (the new "expert juniors") bring deep domain knowledge, and now that they come from within our businesses, they have direct useful experience. They can build prototypes with AI assistance that solve very specific problems that impact only their jobs. Stuff they would struggle to ask an engineer to help them with (if they were lucky enough to have an engineer contact in the company).

We used to consider someone junior when they were a _Classically Trained Entry-Level Engineer_. They have 4 year CS degrees or equivalent experience. They understood coding enough to convince a series of instructors, employers and peers.

These entry-level engineers are some of the people that AI Doomers and AGI-Optimists are trying to convince us that we won't need anymore. The doom and gloom story says that juniors are useless now because a senior has a fleet of juniors to do their work. People (rightly) worry how we will train the next generation of engineers if the engineering org is just senior talent now.

## The Full-Stack Team

Engineering needs to spread out across the entire company. We need to redefine the full-stack team
to span from accounting to engineering. Including a designer and a product manager on our team is no
longer "full-stack" enough. We need a gradient of engineers embedded in every part of our company.

To get the most of their expertise, we can pair domain experts with entry-level engineers. You get domain expertise (accounting, legal, HR, operations) combined with the growing system thinker with the engineering vocabulary. The entry-level engineer provides a bridge from accounting speak into infrastructure speak. They help the Senior Engineers focus on capital-E engineering.

Senior Engineers become platform builders. In the before times, senior engineers would work with
product managers and designers to understand the domain. Now we need seniors to focus on the hard
stuff. Engineers are domain experts in engineering. They need to create the systems that make
safe self-service possible. They need to support the entry-level and junior experts, help them
level up, and give them the tools to contribute effectively.

This gradient of engineer skill, from viber to junior to senior, improves the efficiency of
each conversation while allowing work to continue at the right level of abstraction, where each
person is most skilled.

## What This Means for Hiring

Now the shift begins: Domain experts are the new junior engineering talent. Entry-level
engineers fresh out of college are the mid level talent. They have a natural progression
to senior while filling a need in the company that keeps seniors effective and productive.

It would be a huge mistake to stop hiring early career "junior" programmers. They're comparatively
less junior than the domain experts that just picked up cursor. A 4-year CS degree puts them
squarely in the middle of the pack, now that the spectrum has doubled.

If we fail to have these juniors at hand, we'll end up spending senior engineering time to prevent
vibe coders from leaking company documents or creating headaches. Worse yet, since engineering
management is afraid to waste their team's time, it's often the managers that deal with these new
vibe coders. The cost of deploying a quick script that generates a power point presentation, when
supported by department lead engineering managers and a platform ops team, is astronomical. This is
not what coding agents were supposed to do.

Get this right, and we unlock an enormous amount of domain expertise that was previously bottlenecked behind engineering teams. Get it wrong, and we create security nightmares, devalue or discourage talent, or destroy efficiency by applying senior engineering time ineffectively.

The technical problems are solvable. We've done this before with the DevOps movement. We can do it again with vibe coding domain experts. It requires acknowledging that the spectrum of "engineer" is now much broader than it used to be and building our teams accordingly.]]></source:markdown>
		</item>
		<item>
			<title>We want Ruby to move forward</title>
			<description>The former maintainers are prepared to transfer RubyGems and Bundler to Matz to move Ruby forward</description>
			<link>https://martinemde.com/blog/move-forward</link>
			<guid isPermaLink="true">https://martinemde.com/blog/move-forward</guid>
			<pubDate>Sat, 25 Oct 2025 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>On September 9, without warning, Ruby Central <a href="https://pup-e.com/blog/goodbye-rubygems/">kicked out the maintainers</a> who have cared for Bundler and RubyGems for over a decade. Ruby Central made these changes against the <a href="https://github.com/ruby/rubygems/blob/master/doc/rubygems/POLICIES.md#committer-access">established project policies</a>, while ignoring all <a href="https://gist.github.com/simi/349d881d16d3d86947945615a47c60ca">objections from the maintainers' team</a>. At the time, <a href="https://rubycentral.org/news/strengthening-the-stewardship-of-rubygems-and-bundler/">Ruby Central claimed</a> these changes were "temporary". However,</p>
<ul>
<li>None of the "temporary" changes made by Ruby Central have been undone, more than six weeks later.</li>
<li>Ruby Central still has not communicated with the removed maintainers about restoring any permissions.</li>
<li>Ruby Central still has not offered "operator agreements" or "contributor agreements" to any of the removed maintainers.</li>
<li>The <a href="https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/merger-agreement.pdf">Ruby Together merger agreement</a> plainly states that it is the maintainers who will decide what is best for their projects, not Ruby Central.</li>
<li><a href="https://www.ruby-lang.org/en/news/2025/10/17/rubygems-repository-transition/">Last week</a>, the Ruby core team, led by Matz, decided to assume stewardship of RubyGems and Bundler GitHub repository.</li>
<li>Ruby Central did not communicate with any removed maintainers before transferring control of the <a href="https://github.com/rubygems/rubygems/">rubygems/rubygems</a> GitHub repo to the Ruby core team.</li>
<li>On October 24th, Shan publicly confirmed <a href="https://youtu.be/nKpo68g9dEk?list=PLdqi4WM39BUiorBaKf4KfhejVDm0Uu0ew&#x26;t=823">she does not believe the maintainers need to be told why they were removed</a>.</li>
</ul>
<p>While we know that Ruby Central had no right to act the way they did, it is nevertheless clear to us that the Ruby community will be better off if the codebase, maintenance, and legal rights to RubyGems and Bundler are all together in the same place.</p>
<p>To bring this about, <strong>we are prepared to transfer our interests in RubyGems and Bundler to Matz</strong>, end the dispute over the GitHub enterprise account, 2 GitHub organizations, and 70 repositories, and hand over all rights in the Bundler logo and Bundler name, including the trademark applications in the US, EU, and Japan.</p>
<p>Once we have entered into a legal agreement to settle any legal claims with Ruby Central and transfer all rights to Matz, the former maintainers will step back entirely from the RubyGems and Bundler projects, leaving them fully and completely to Matz, and by extension to the entire Ruby community.</p>
<p>Although Ruby Central's actions were not legitimate, our commitment to the Ruby community remains strong. We're choosing to focus our energy on projects to improve Ruby for everyone, including <a href="https://rv.dev/">rv</a>, <a href="https://github.com/RubyElders/ruby-butler">Ruby Butler</a>, <a href="https://github.com/duckinator/jim">jim</a>, and <a href="https://gem.coop/">gem.coop</a>.</p>
<p>Signed,
The former maintainers: Martin, André, David, Ellen, Josef, and Samuel</p>
]]></content:encoded>
			<source:markdown><![CDATA[On September 9, without warning, Ruby Central [kicked out the maintainers](https://pup-e.com/blog/goodbye-rubygems/) who have cared for Bundler and RubyGems for over a decade. Ruby Central made these changes against the [established project policies](https://github.com/ruby/rubygems/blob/master/doc/rubygems/POLICIES.md#committer-access), while ignoring all [objections from the maintainers' team](https://gist.github.com/simi/349d881d16d3d86947945615a47c60ca). At the time, [Ruby Central claimed](https://rubycentral.org/news/strengthening-the-stewardship-of-rubygems-and-bundler/) these changes were "temporary". However,

- None of the "temporary" changes made by Ruby Central have been undone, more than six weeks later.
- Ruby Central still has not communicated with the removed maintainers about restoring any permissions.
- Ruby Central still has not offered "operator agreements" or "contributor agreements" to any of the removed maintainers.
- The [Ruby Together merger agreement](https://andre.arko.net/2025/09/25/bundler-belongs-to-the-ruby-community/merger-agreement.pdf) plainly states that it is the maintainers who will decide what is best for their projects, not Ruby Central.
- [Last week](https://www.ruby-lang.org/en/news/2025/10/17/rubygems-repository-transition/), the Ruby core team, led by Matz, decided to assume stewardship of RubyGems and Bundler GitHub repository.
- Ruby Central did not communicate with any removed maintainers before transferring control of the [rubygems/rubygems](https://github.com/rubygems/rubygems/) GitHub repo to the Ruby core team.
- On October 24th, Shan publicly confirmed [she does not believe the maintainers need to be told why they were removed](https://youtu.be/nKpo68g9dEk?list=PLdqi4WM39BUiorBaKf4KfhejVDm0Uu0ew&t=823).

While we know that Ruby Central had no right to act the way they did, it is nevertheless clear to us that the Ruby community will be better off if the codebase, maintenance, and legal rights to RubyGems and Bundler are all together in the same place.

To bring this about, **we are prepared to transfer our interests in RubyGems and Bundler to Matz**, end the dispute over the GitHub enterprise account, 2 GitHub organizations, and 70 repositories, and hand over all rights in the Bundler logo and Bundler name, including the trademark applications in the US, EU, and Japan.

Once we have entered into a legal agreement to settle any legal claims with Ruby Central and transfer all rights to Matz, the former maintainers will step back entirely from the RubyGems and Bundler projects, leaving them fully and completely to Matz, and by extension to the entire Ruby community.

Although Ruby Central's actions were not legitimate, our commitment to the Ruby community remains strong. We're choosing to focus our energy on projects to improve Ruby for everyone, including [rv](https://rv.dev/), [Ruby Butler](https://github.com/RubyElders/ruby-butler), [jim](https://github.com/duckinator/jim), and [gem.coop](https://gem.coop/).

Signed,
The former maintainers: Martin, André, David, Ellen, Josef, and Samuel]]></source:markdown>
		</item>
		<item>
			<title>Announcing The Gem Cooperative</title>
			<description>don&apos;t put all your gems in one basket - The former team from rubygems.org is excited to announce a new gem server for the Ruby community</description>
			<link>https://martinemde.com/blog/announcing-gem-coop</link>
			<guid isPermaLink="true">https://martinemde.com/blog/announcing-gem-coop</guid>
			<pubDate>Sun, 05 Oct 2025 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<h5><em>don't put all your gems in one basket</em></h5>
<p>The former team from rubygems.org is excited to announce a new gem server for the Ruby community:</p>
<p><a href="https://gem.coop">https://gem.coop</a></p>
<p>Right now, the current versions of RubyGems and Bundler work with this new server. All Ruby developers are welcome to switch to using this new server immediately. Just swap your primary source to <code>https://gem.coop</code></p>
<p>The gem.coop server is simple and we aim to keep it that way. We plan to add features that serve to increase security and make packaging blazing fast. We want the default choice to be the secure choice.</p>
<h2>Join us</h2>
<p>You can <a href="https://join.slack.com/t/bundler/shared_invite/zt-3ey89pd2b-gM4LAB8Xr921Ki_SEKAx4w">join us on the Bundler Slack</a> to get involved in the discussion.</p>
<h2>Questions?</h2>
<h3>What is the .coop TLD? Is it about chickens?</h3>
<p>Right!? I'm glad you started with a softball question. I think it <em>should</em> be chicken related (logo idea!). The <code>.coop</code> or "Co-op" stands for "Cooperative", like REI or a local credit union. It's part of the way we're imagining that we can support ruby packaging in a way that's both sustainable, open, and fair.</p>
<h3>Is it stable?</h3>
<p>Gem.coop is served with full, fast, edge caching and all gems are immediately available upon push to rubygems.org</p>
<h3>Can I publish gems to gem.coop?</h3>
<p>Not yet. There's an interesting and tricky problem to solve in order to have two divergent public gem servers. This is our focus for the coming months. We know people are excited to have an alternative and we hope to solve this soon.</p>
<h3>How will you handle governance?</h3>
<p>We're working on Gem.coop governance with the support of Mike McQuaid, Project Lead of Homebrew. We aim to release publicly later this week.</p>
<h3>Can I donate?</h3>
<p>If you're feeling gracious you can always support one or all of <a href="https://github.com/orgs/gem-coop/people">the maintainers on GitHub</a>. It's very kind of you read this question and imagine asking if you could donate. Thank you!</p>
<p>We didn't feel ready to ask anyone for money yet, so if you want to contribute to the project, you can support us by sharing this post and showing your excitement, testing out the server, supporting the team, and generally being a good part of the community.</p>
<h2>Now what?</h2>
<p>Join <a href="https://gem.coop">the mailing list</a> and we'll keep you updated. No corporatespeak, just developers excited about Ruby and Gem packaging.</p>
<p>Thanks to all my teammates that make this possible!
@simi, @segiddins, @indirect, @duckinator, and @deivid-rodriguez</p>
]]></content:encoded>
			<source:markdown><![CDATA[##### _don't put all your gems in one basket_

The former team from rubygems.org is excited to announce a new gem server for the Ruby community:

[https://gem.coop](https://gem.coop)

Right now, the current versions of RubyGems and Bundler work with this new server. All Ruby developers are welcome to switch to using this new server immediately. Just swap your primary source to `https://gem.coop`

The gem.coop server is simple and we aim to keep it that way. We plan to add features that serve to increase security and make packaging blazing fast. We want the default choice to be the secure choice.

## Join us

You can [join us on the Bundler Slack](https://join.slack.com/t/bundler/shared_invite/zt-3ey89pd2b-gM4LAB8Xr921Ki_SEKAx4w) to get involved in the discussion.

## Questions?

### What is the .coop TLD? Is it about chickens?

Right!? I'm glad you started with a softball question. I think it _should_ be chicken related (logo idea!). The `.coop` or "Co-op" stands for "Cooperative", like REI or a local credit union. It's part of the way we're imagining that we can support ruby packaging in a way that's both sustainable, open, and fair.

### Is it stable?

Gem.coop is served with full, fast, edge caching and all gems are immediately available upon push to rubygems.org

### Can I publish gems to gem.coop?

Not yet. There's an interesting and tricky problem to solve in order to have two divergent public gem servers. This is our focus for the coming months. We know people are excited to have an alternative and we hope to solve this soon.

### How will you handle governance?

We're working on Gem.coop governance with the support of Mike McQuaid, Project Lead of Homebrew. We aim to release publicly later this week.

### Can I donate?

If you're feeling gracious you can always support one or all of [the maintainers on GitHub](https://github.com/orgs/gem-coop/people). It's very kind of you read this question and imagine asking if you could donate. Thank you!

We didn't feel ready to ask anyone for money yet, so if you want to contribute to the project, you can support us by sharing this post and showing your excitement, testing out the server, supporting the team, and generally being a good part of the community.

## Now what?

Join [the mailing list](https://gem.coop) and we'll keep you updated. No corporatespeak, just developers excited about Ruby and Gem packaging.

Thanks to all my teammates that make this possible!
@simi, @segiddins, @indirect, @duckinator, and @deivid-rodriguez]]></source:markdown>
		</item>
		<item>
			<title>How to convert to params.expect in Rails 8.0</title>
			<description>A practical guide to converting existing Rails controllers to use params.expect</description>
			<link>https://martinemde.com/blog/how-to-convert-to-rails-params-expect</link>
			<guid isPermaLink="true">https://martinemde.com/blog/how-to-convert-to-rails-params-expect</guid>
			<pubDate>Sat, 21 Dec 2024 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>After updating RubyGems.org to use <a href="https://martinemde.com/blog/how-to-rails-params-expect">the new <code>params.expect</code> feature</a>
in Rails 8, I thought it might be helpful to go over a few of the challenges I ran into.</p>
<h2>Why Should I Convert?</h2>
<p>The new <code>expect</code> method for filtering params protects against user param tampering that can cause hard to rescue errors.</p>
<p>As a quick review of the feature, when we have code like this:</p>
<pre><code class="language-ruby">user_params = params.require(:user).permit(:name, :handle)
</code></pre>
<p>We're vulnerable to users calling our action like this:</p>
<pre><code class="language-ruby">post "/users", params: { user: "error" }
user_params = params.require(:user).permit(:name, :handle)
# undefined method `permit' for an instance of String
</code></pre>
<p>By using the new <code>params.expect</code> we can prevent this problem (and another I'll discuss below) all while cleaning up our params handling.</p>
<pre><code class="language-ruby">post "/users", params: { user: "error" }
user_params = params.expect(user: [:name, :handle])
# responds as if the required :user key was not sent at all, rendering a 400 error
</code></pre>
<p>Remember that with valid params, the values at the expected key(s) will be returned, just like with <code>require</code>.</p>
<pre><code class="language-ruby">post "/users", params: { user: { name: "Martin", handle: "martinemde" }
user_params = params.expect(user: [:name, :handle])
# => { name: "Martin", handle: "martinemde" }
</code></pre>
<h2>The Easy Part</h2>
<p>The conversion follows a consistent pattern.</p>
<pre><code class="language-ruby"># OLD
user_params = params.require(:user).permit(:name, :handle)

# NEW
user_params = params.expect(user: [:name, :handle])
</code></pre>
<p>For methods that permit a mix of scalars and hashes, the conversion is also straightforward:</p>
<pre><code class="language-ruby"># OLD
user_params = params.require(:user).permit(:name, :handle, :image, address: [:street, :city])

# NEW
user_params = params.expect(user: [:name, :handle, :image, { address: [:street, :city] }])
</code></pre>
<h2>The Challenge</h2>
<p>The challenge comes when you have conditional or complex parameter handling, especially when you're doing something like this:</p>
<pre><code class="language-ruby"># OLD: Conditional parameters
user_params = params.require(:user).permit(:name, :handle)
user_params[:admin] = true if current_user.admin?

# OR

# OLD: Multiple permit calls
base_params = params.require(:user).permit(:name, :handle)
admin_params = params.require(:user).permit(:admin) if current_user.admin?
user_params = base_params.merge(admin_params || {})
</code></pre>
<h2>Solution 1: Extract All Parameters First</h2>
<p>The simplest approach is to extract all the parameters you might need and then conditionally use them:</p>
<pre><code class="language-ruby"># NEW: Extract all potential parameters
if current_user.admin?
  user_params = params.expect(user: [:name, :handle, :admin])
else
  user_params = params.expect(user: [:name, :handle])
end
</code></pre>
<p>Or more concisely:</p>
<pre><code class="language-ruby"># NEW: Conditional parameter list
permitted_keys = [:name, :handle]
permitted_keys &#x3C;&#x3C; :admin if current_user.admin?
user_params = params.expect(user: permitted_keys)
</code></pre>
<h2>Solution 2: Use Multiple expect Calls</h2>
<p>For complex cases, you can still use multiple <code>expect</code> calls:</p>
<pre><code class="language-ruby"># NEW: Multiple expect calls
user_params = params.expect(user: [:name, :handle])
user_params[:admin] = params.expect(user: [:admin])[:admin] if current_user.admin?
</code></pre>
<p>Though this is less elegant than the single-call approach.</p>
<h2>Solution 3: Helper Methods</h2>
<p>For really complex parameter handling, consider extracting logic into helper methods:</p>
<pre><code class="language-ruby">private

def user_params
  if current_user.admin?
    params.expect(user: [:name, :handle, :admin])
  else
    params.expect(user: [:name, :handle])
  end
end
</code></pre>
<h2>Testing Your Conversion</h2>
<p>When converting to <code>params.expect</code>, make sure to test edge cases:</p>
<pre><code class="language-ruby"># Test with tampered params
test "handles tampered user param as string" do
  post users_path, params: { user: "tampered" }
  assert_response :bad_request
end

test "handles tampered user param as array" do
  post users_path, params: { user: ["tampered"] }
  assert_response :bad_request
end

test "handles missing user param" do
  post users_path, params: {}
  assert_response :bad_request
end
</code></pre>
<h2>My Experience Converting RubyGems.org</h2>
<p>Beyond security improvements, <code>params.expect</code> also provides clearer intent with parameter structure explicitly declared. You get better error messages with more specific feedback, and more consistent behavior with less underspecified edge cases.</p>
<p>When I converted RubyGems.org to use <code>params.expect</code>, I found that most conversions were straightforward. Complex parameter handling became more explicit and easier to understand. We caught several edge cases we hadn't properly handled before and the code became more self-documenting.</p>
<p>Converting to <code>params.expect</code> improves security and code clarity. Start with the simple cases, and don't be afraid to use conditional logic or helper methods for complex parameter handling.</p>
<p>The key is to think about your parameter structure upfront and declare it explicitly rather than building it up incrementally.</p>
]]></content:encoded>
			<source:markdown><![CDATA[After updating RubyGems.org to use [the new `params.expect` feature](/blog/how-to-rails-params-expect)
in Rails 8, I thought it might be helpful to go over a few of the challenges I ran into.

## Why Should I Convert?

The new `expect` method for filtering params protects against user param tampering that can cause hard to rescue errors.

As a quick review of the feature, when we have code like this:

```ruby
user_params = params.require(:user).permit(:name, :handle)
```

We're vulnerable to users calling our action like this:

```ruby
post "/users", params: { user: "error" }
user_params = params.require(:user).permit(:name, :handle)
# undefined method `permit' for an instance of String
```

By using the new `params.expect` we can prevent this problem (and another I'll discuss below) all while cleaning up our params handling.

```ruby
post "/users", params: { user: "error" }
user_params = params.expect(user: [:name, :handle])
# responds as if the required :user key was not sent at all, rendering a 400 error
```

Remember that with valid params, the values at the expected key(s) will be returned, just like with `require`.

```ruby
post "/users", params: { user: { name: "Martin", handle: "martinemde" }
user_params = params.expect(user: [:name, :handle])
# => { name: "Martin", handle: "martinemde" }
```

## The Easy Part

The conversion follows a consistent pattern.

```ruby
# OLD
user_params = params.require(:user).permit(:name, :handle)

# NEW
user_params = params.expect(user: [:name, :handle])
```

For methods that permit a mix of scalars and hashes, the conversion is also straightforward:

```ruby
# OLD
user_params = params.require(:user).permit(:name, :handle, :image, address: [:street, :city])

# NEW
user_params = params.expect(user: [:name, :handle, :image, { address: [:street, :city] }])
```

## The Challenge

The challenge comes when you have conditional or complex parameter handling, especially when you're doing something like this:

```ruby
# OLD: Conditional parameters
user_params = params.require(:user).permit(:name, :handle)
user_params[:admin] = true if current_user.admin?

# OR

# OLD: Multiple permit calls
base_params = params.require(:user).permit(:name, :handle)
admin_params = params.require(:user).permit(:admin) if current_user.admin?
user_params = base_params.merge(admin_params || {})
```

## Solution 1: Extract All Parameters First

The simplest approach is to extract all the parameters you might need and then conditionally use them:

```ruby
# NEW: Extract all potential parameters
if current_user.admin?
  user_params = params.expect(user: [:name, :handle, :admin])
else
  user_params = params.expect(user: [:name, :handle])
end
```

Or more concisely:

```ruby
# NEW: Conditional parameter list
permitted_keys = [:name, :handle]
permitted_keys << :admin if current_user.admin?
user_params = params.expect(user: permitted_keys)
```

## Solution 2: Use Multiple expect Calls

For complex cases, you can still use multiple `expect` calls:

```ruby
# NEW: Multiple expect calls
user_params = params.expect(user: [:name, :handle])
user_params[:admin] = params.expect(user: [:admin])[:admin] if current_user.admin?
```

Though this is less elegant than the single-call approach.

## Solution 3: Helper Methods

For really complex parameter handling, consider extracting logic into helper methods:

```ruby
private

def user_params
  if current_user.admin?
    params.expect(user: [:name, :handle, :admin])
  else
    params.expect(user: [:name, :handle])
  end
end
```

## Testing Your Conversion

When converting to `params.expect`, make sure to test edge cases:

```ruby
# Test with tampered params
test "handles tampered user param as string" do
  post users_path, params: { user: "tampered" }
  assert_response :bad_request
end

test "handles tampered user param as array" do
  post users_path, params: { user: ["tampered"] }
  assert_response :bad_request
end

test "handles missing user param" do
  post users_path, params: {}
  assert_response :bad_request
end
```

## My Experience Converting RubyGems.org

Beyond security improvements, `params.expect` also provides clearer intent with parameter structure explicitly declared. You get better error messages with more specific feedback, and more consistent behavior with less underspecified edge cases.

When I converted RubyGems.org to use `params.expect`, I found that most conversions were straightforward. Complex parameter handling became more explicit and easier to understand. We caught several edge cases we hadn't properly handled before and the code became more self-documenting.

Converting to `params.expect` improves security and code clarity. Start with the simple cases, and don't be afraid to use conditional logic or helper methods for complex parameter handling.

The key is to think about your parameter structure upfront and declare it explicitly rather than building it up incrementally.]]></source:markdown>
		</item>
		<item>
			<title>How to: Rails params.expect</title>
			<description>A guide to the new params.expect feature in Rails 8</description>
			<link>https://martinemde.com/blog/how-to-rails-params-expect</link>
			<guid isPermaLink="true">https://martinemde.com/blog/how-to-rails-params-expect</guid>
			<pubDate>Tue, 22 Oct 2024 12:00:00 GMT</pubDate>
			
			<content:encoded><![CDATA[<p>In this post I will describe <a href="https://github.com/rails/rails/pull/51674">the new <code>params.expect</code> feature</a> that I recently added to Rails 8 and go over how it works and how to use it.</p>
<p>Update: For an implementation guide, see <a href="https://martinemde.com/blog/how-to-convert-to-rails-params-expect">my second post on <code>params.expect</code></a>.</p>
<h2>params: An Attack Vector</h2>
<p>All web application programmers learn not to trust user input.</p>
<p>Rails has long provided a simple pattern to prevent param tampering: <code>params.permit</code>. This protects our app from users that may alter params in order to insert attributes or alter behavior, like assigning admin to yourself.</p>
<pre><code class="language-ruby">def update
  user_params = params.require(:user).permit(:name, :favorite_pie)

  if @user.update(user_params)
    redirect_to :user
  else
    render :edit
  end
end
</code></pre>
<p>This works great when users submit the form correctly and even when they try to insert extra fields, like <code>admin=true</code> into the params. These attacks get filtered out.</p>
<p>But protecting us from correctly submitted forms and extra attributes is not enough. What if, as we regularly see on RubyGems.org, a user is trying to break your application by submitting malformed params? Problems start to emerge.</p>
<p>The solution in Rails 8 is the new <code>params.expect</code>.</p>
<h2>How do I use params.expect?</h2>
<p>If you don't want to dig into Rails parameter filtering right now, you can simply do the following in Rails 8:</p>
<pre><code class="language-ruby">def update
  # OLD
  # user_params = params.require(:user).permit(:name, :favorite_pie)

  # NEW (see how it mirrors the expected params?)
  user_params = params.expect(user: [:name, :favorite_pie])

  if @user.update(user_params)
    redirect_to :user
  else
    render :edit
  end
end
</code></pre>
<p>The <code>expect</code> method behaves very similarly to <code>require(:user).permit(:name, :favorite_pie)</code>, but it is much more explicit about exactly what the parameters should look like and rejects any params that don't exactly match the expected structure.</p>
<p>Let's be very clear about what each type of parameter looks like and how to declare them in <code>expect</code>:</p>
<pre><code class="language-ruby"># A Hash:
PATCH /users/1/?user[name]=martin&#x26;user[favorite_pie]=pumpkin
# params = { "user" => { "name" => "martin", "favorit_pie" => "pumpkin" } }

# An Array:
POST /pies?pies[][flavor]=pumpkin&#x26;pies[][flavor]=pecan
# params = { "pies" => [
#            { "flavor" => "pumpkin" },
#            { "flavor" => "pecan" }
#          ] }

# A String:
POST /search?q=hello+world
# params = { "q" => "hello world" }
</code></pre>
<p>Each parameter type needs to be declared slightly differently in <code>expect</code>:</p>
<pre><code class="language-ruby"># Extract a Hash containing name and favorite_pie keys
params.expect(user: [:name, :favorite_pie])

# Extract an Array containing Hashes, each with a :flavor key
# (note: the Array syntax is more explicit than `permit`)
params.expect(pies: [{ flavor: [] }])
#     Note the explicit Array format above: ^^         ^^

# Extract a String
params.expect(:q)
</code></pre>
<p>The rest of this post will dig into why and how the new syntax works, how to use it, and some of the gotchas that you might experience with this change.</p>
<h2>The Problem of Params Tampering</h2>
<p>Let's start by exploring the core problem this new feature solves: params tampering that causes uncaught exceptions.</p>
<p>Here's what can happen with the traditional Rails approach when params are tampered with:</p>
<pre><code class="language-ruby">class UsersController &#x3C; ApplicationController
  def update
    user_params = params.require(:user).permit(:name, :favorite_pie)

    if @user.update(user_params)
      redirect_to @user
    else
      render :edit
    end
  end
end
</code></pre>
<h3>When params are tampered to be a String</h3>
<pre><code class="language-ruby"># Normal form submission:
# params = { "user" => { "name" => "martin", "favorite_pie" => "pumpkin" } }

# Tampered to be a String:
# params = { "user" => "tampered_string" }

user_params = params.require(:user).permit(:name, :favorite_pie)
# => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
</code></pre>
<h3>When params are tampered to be an Array</h3>
<pre><code class="language-ruby"># Tampered to be an Array:
# params = { "user" => ["tampered", "array"] }

user_params = params.require(:user).permit(:name, :favorite_pie)
# => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
</code></pre>
<p>Both of these examples show how parameter tampering can cause unhandled exceptions in your Rails application, leading to 500 errors for your users instead of graceful error handling.</p>
<h2>What's going on exactly?</h2>
<p>The problem happens when <code>params.require(:user)</code> returns something that doesn't respond to <code>permit</code>. When the user parameter is a string or array instead of a hash, <code>permit</code> fails.</p>
<p>The old workaround was something ugly like this:</p>
<pre><code class="language-ruby"># ugly
user_params = params.require(:user)
raise ActionController::ParameterMissing.new(:user, {}) unless user_params.respond_to?(:permit)
user_params = user_params.permit(:name, :favorite_pie)
</code></pre>
<p>I discovered a better version when I was trying to solve this problem for RubyGems.org. It has a few flaws, but it works well enough.</p>
<pre><code class="language-ruby"># better
user_params = params.fetch(:user, {}).permit(:name, :favorite_pie)
raise ActionController::ParameterMissing.new(:user, {}) if user_params.empty?
</code></pre>
<p><code>params.expect</code> replaces all of this mess with clear, declarative syntax that also provides better error messages and handles edge cases properly.</p>
<h2>params.expect to the rescue</h2>
<p>The new <code>expect</code> method validates both the presence and structure of the parameters:</p>
<pre><code class="language-ruby"># This expects :user to be a Hash with :name and :favorite_pie keys
user_params = params.expect(user: [:name, :favorite_pie])

# If user is missing:
# => ActionController::ParameterMissing

# If user is not a Hash (e.g., String or Array):
# => ActionController::ParameterMissing
</code></pre>
<p>By resolving this ambiguity in params parsing and improving the syntax and security of features used by every Rails engineer, we are able to reduce false alarms, better protect application data, and maybe reduce the number of engineers getting paged in the middle of the night.</p>
]]></content:encoded>
			<source:markdown><![CDATA[In this post I will describe [the new `params.expect` feature](https://github.com/rails/rails/pull/51674) that I recently added to Rails 8 and go over how it works and how to use it.

Update: For an implementation guide, see [my second post on `params.expect`](/blog/how-to-convert-to-rails-params-expect).

## params: An Attack Vector

All web application programmers learn not to trust user input.

Rails has long provided a simple pattern to prevent param tampering: `params.permit`. This protects our app from users that may alter params in order to insert attributes or alter behavior, like assigning admin to yourself.

```ruby
def update
  user_params = params.require(:user).permit(:name, :favorite_pie)

  if @user.update(user_params)
    redirect_to :user
  else
    render :edit
  end
end
```

This works great when users submit the form correctly and even when they try to insert extra fields, like `admin=true` into the params. These attacks get filtered out.

But protecting us from correctly submitted forms and extra attributes is not enough. What if, as we regularly see on RubyGems.org, a user is trying to break your application by submitting malformed params? Problems start to emerge.

The solution in Rails 8 is the new `params.expect`.

## How do I use params.expect?

If you don't want to dig into Rails parameter filtering right now, you can simply do the following in Rails 8:

```ruby
def update
  # OLD
  # user_params = params.require(:user).permit(:name, :favorite_pie)

  # NEW (see how it mirrors the expected params?)
  user_params = params.expect(user: [:name, :favorite_pie])

  if @user.update(user_params)
    redirect_to :user
  else
    render :edit
  end
end
```

The `expect` method behaves very similarly to `require(:user).permit(:name, :favorite_pie)`, but it is much more explicit about exactly what the parameters should look like and rejects any params that don't exactly match the expected structure.

Let's be very clear about what each type of parameter looks like and how to declare them in `expect`:

```ruby
# A Hash:
PATCH /users/1/?user[name]=martin&user[favorite_pie]=pumpkin
# params = { "user" => { "name" => "martin", "favorit_pie" => "pumpkin" } }

# An Array:
POST /pies?pies[][flavor]=pumpkin&pies[][flavor]=pecan
# params = { "pies" => [
#            { "flavor" => "pumpkin" },
#            { "flavor" => "pecan" }
#          ] }

# A String:
POST /search?q=hello+world
# params = { "q" => "hello world" }
```

Each parameter type needs to be declared slightly differently in `expect`:

```ruby
# Extract a Hash containing name and favorite_pie keys
params.expect(user: [:name, :favorite_pie])

# Extract an Array containing Hashes, each with a :flavor key
# (note: the Array syntax is more explicit than `permit`)
params.expect(pies: [{ flavor: [] }])
#     Note the explicit Array format above: ^^         ^^

# Extract a String
params.expect(:q)
```

The rest of this post will dig into why and how the new syntax works, how to use it, and some of the gotchas that you might experience with this change.

## The Problem of Params Tampering

Let's start by exploring the core problem this new feature solves: params tampering that causes uncaught exceptions.

Here's what can happen with the traditional Rails approach when params are tampered with:

```ruby
class UsersController < ApplicationController
  def update
    user_params = params.require(:user).permit(:name, :favorite_pie)

    if @user.update(user_params)
      redirect_to @user
    else
      render :edit
    end
  end
end
```

### When params are tampered to be a String

```ruby
# Normal form submission:
# params = { "user" => { "name" => "martin", "favorite_pie" => "pumpkin" } }

# Tampered to be a String:
# params = { "user" => "tampered_string" }

user_params = params.require(:user).permit(:name, :favorite_pie)
# => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
```

### When params are tampered to be an Array

```ruby
# Tampered to be an Array:
# params = { "user" => ["tampered", "array"] }

user_params = params.require(:user).permit(:name, :favorite_pie)
# => ActionController::UnfilteredParameters: unable to convert unpermitted parameters to hash
```

Both of these examples show how parameter tampering can cause unhandled exceptions in your Rails application, leading to 500 errors for your users instead of graceful error handling.

## What's going on exactly?

The problem happens when `params.require(:user)` returns something that doesn't respond to `permit`. When the user parameter is a string or array instead of a hash, `permit` fails.

The old workaround was something ugly like this:

```ruby
# ugly
user_params = params.require(:user)
raise ActionController::ParameterMissing.new(:user, {}) unless user_params.respond_to?(:permit)
user_params = user_params.permit(:name, :favorite_pie)
```

I discovered a better version when I was trying to solve this problem for RubyGems.org. It has a few flaws, but it works well enough.

```ruby
# better
user_params = params.fetch(:user, {}).permit(:name, :favorite_pie)
raise ActionController::ParameterMissing.new(:user, {}) if user_params.empty?
```

`params.expect` replaces all of this mess with clear, declarative syntax that also provides better error messages and handles edge cases properly.

## params.expect to the rescue

The new `expect` method validates both the presence and structure of the parameters:

```ruby
# This expects :user to be a Hash with :name and :favorite_pie keys
user_params = params.expect(user: [:name, :favorite_pie])

# If user is missing:
# => ActionController::ParameterMissing

# If user is not a Hash (e.g., String or Array):
# => ActionController::ParameterMissing
```

By resolving this ambiguity in params parsing and improving the syntax and security of features used by every Rails engineer, we are able to reduce false alarms, better protect application data, and maybe reduce the number of engineers getting paged in the middle of the night.]]></source:markdown>
		</item>
	</channel>
</rss>