Homebrew 7.0.0 Shipped the Agent Security Fix Everyone Else Is Still Writing Prompts For
Homebrew replaced arbitrary Ruby install hooks with a closed set of declared steps that take literal arguments. If you run agents with shell access, that migration is the template you have been looking for.
For seventeen years, installing a package with Homebrew meant running a stranger's Ruby. Not metaphorically. A formula's post_install block was a method body, and whatever was in it executed on your machine with your privileges, at your prompt, because you typed three words and pressed return. Every package manager works roughly this way and almost nobody thinks about it, because the alternative always sounded like a research project.
Homebrew 7.0.0, released on September 13, is that research project shipped. The release notes lead with speed, a native macOS app, and Intel Macs moving to Tier 3. Buried under "Tap maintainers" is the change that actually matters: post_install is deprecated, replaced by post_install_steps, and a post_install_steps block "may only contain the supported step calls with literal arguments. It cannot call the wider formula DSL or arbitrary Ruby code."
That sentence is the whole thing. A package author no longer describes what to run. They declare what they want done, from a finite list, and Homebrew decides how.
I want to make the case that this is the most useful engineering document published this week for anyone building with agents, and that it is going to get filed as a Mac release note and forgotten.
The problem is not trust, it is expressiveness
Here is the shape of the problem in both domains.
A package manager has a formula written by someone you have never met. An agent harness has a tool call generated by a model whose reasoning you cannot inspect. In both cases, a thing you did not write ends up deciding what runs on your machine. In both cases the industry's first answer was a review process and a strongly worded policy, and in both cases that answer is load-bearing on somebody reading carefully and meaning well.
The reflex fix is to add supervision. Audit the formula. Ask the model to confirm before destructive actions. Put a human in the loop. All of that helps and none of it changes the underlying fact, which is that the interface accepts arbitrary computation. If your tool surface is "run this shell command," the set of things that can happen is the set of things a shell can do, and no amount of policy narrows it. You are reviewing an infinite space by sampling.
What Homebrew did instead was narrow the space. The supported file-preparation steps are mkdir_p, touch, move, move_contents, copy, remove, inreplace, symlink, symlink_tree, symlink_children, set_permissions, and change_dylib_id. There is write_file for default configs, init_data_dir for database service directories, a set of desktop and cache rebuilds like update_gtk_icon_cache and compile_gsettings_schemas, and a set of named actions for families of formulae that share an algorithm, such as configure_gcc_runtime and bootstrap_cpython. Guards are if_path_exists, unless_path_exists, on_macos and on_linux. Paths take a base: such as :var, :etc or :pkgetc rather than a raw string you construct yourself. Call it thirty or so steps in total.
Now the part I got wrong on first read, which is also the most interesting part. Homebrew did not remove command execution. There is a run step. The Formula Cookbook describes it this way: "run executes one command with a literal argument array; it does not evaluate a shell command string. Select the executable with base:, such as :bin, :libexec or :homebrew_prefix, or pass an absolute system executable."
Read that twice, because the second clause is doing all the work. The thing that got deleted is not execution. It is string evaluation. A formula can still run a helper it installed into libexec. It cannot compose a command out of pieces at install time and hand the result to a shell, because there is no shell in the path and the arguments are a literal array rather than a string. There is also terminate_process, which kills a process by name or full command line, and warn, which emits a literal warning, and that is the entire command and lifecycle category.
And there is still no network verb anywhere in the set. A formula can create a directory under var, write a config into etc, and invoke a binary it shipped, and it cannot phone home, because the vocabulary has no word for phoning home.
Compare that to the way most agent harnesses are secured today, which is a bash tool that accepts a string, plus a paragraph in a system prompt asking the model not to run destructive commands.
What "declared" buys that "reviewed" does not
Three things, and the third one is the one people miss.
The steps are inspectable without execution. Homebrew's docs note that steps "are stored in the JSON API and do not require downloading source formula Ruby." A Ruby method body has to be run, or at least parsed and reasoned about, to know what it does. A list of declared operations with literal arguments is just data. You can diff it. You can grep it. You can compute over a hundred thousand of them and ask which formulae touch etc. None of that was possible before, at any price.
The steps can be validated before anything happens. Homebrew validates serialised steps ahead of execution, and it delivers them as signed data. The check runs at a point where failing costs nothing. Contrast with the usual agent pattern, where you find out what the command did by reading what it did.
The declaration is the same object the security control operates on. This is the subtle one. In the old model there were two artifacts: the Ruby the author wrote, and whatever policy or sandbox you wrapped around it. They could drift. The policy could be wrong about the Ruby. In the new model there is one artifact, and the enforcement reads the same bytes the author wrote. There is no gap to fall through because there is no second representation.
If you have ever written an agent permission system with an allowlist of command patterns and then watched a model get around it with env -C or an eval wrapper, you have felt the cost of that gap. The allowlist was reasoning about a string. The shell was reasoning about a program. Two representations, one of them wrong.
The part that makes it real: they deleted the escape hatches
Plenty of projects ship a safer default and then leave a flag to turn it off, which means the safer default is a suggestion with extra steps. Homebrew did not do that, and the migration guide is where you can see it.
HOMEBREW_NO_SANDBOX_CASK: disabled, and the replacement column reads "No sandbox bypass replacement." Cask pkg allow_untrusted:: deprecated, replacement "Trusted package; no bypass replacement." HOMEBREW_EVAL_ALL and --eval-all across brew deps, desc, info, options, readall, search, uses and tap: disabled, with trusted-tap evaluation now the default. HOMEBREW_ALLOWED_TAPS: deprecated, "No replacement allowlist setting." HOMEBREW_NO_REQUIRE_TAP_TRUST: deprecated, and you are pointed at brew trust per tap instead.
Official taps already reject the legacy hooks outright. Third-party taps get warnings until December 11, 2027. brew style --fix converts the common patterns automatically. So the sequence is: build the narrow path, make it the default, remove the way around it, and give the ecosystem fifteen months to walk over. Not a blog post asking people to be careful.
Alongside the hook change, formula and cask operations now run sandboxed. Migrated formulae download in a fetch phase with network access, then install with networking disabled and those caches read only. Sandboxed reads of the home directory are blocked by default. On Linux, Bubblewrap was replaced with Landlock, which needs no dependencies and no elevated Docker permissions.
None of this is a wall, and McQuaid says so plainly in the post. Tap trust, not the sandbox, remains the primary protection. Applications still execute with the user's privileges once installed. Vendor .pkg installers run outside the sandbox entirely and may require sudo. The honesty in that paragraph is part of why I trust the rest of it.
Put this into practice
You are probably not maintaining a Homebrew tap. The transferable part is the method, and it fits on an index card.
Start by writing down the verbs your agent actually needs. Not the tools you gave it. The operations it performs when it is doing the job right. For most internal agents the real list is short: read a file, write a file, run one of four or five named scripts, call two internal APIs, post a message. If your answer is "well, it needs bash," that is not an answer, that is the absence of one. Spend an hour reading a week of your agent's actual tool calls and tally what it did. The list will be shorter than you expect and it will surprise you twice, once at how repetitive it is and once at the one weird thing in there.
Replace the general tool with the specific ones. A run_migration(name) tool that takes a migration name from a known set is a different security object than a bash tool that happens to be used for migrations. The model loses nothing it needed. You gain a surface you can enumerate.
Take the argument array, not the command string. This is the single highest-value change and it is the one Homebrew kept when it deleted everything else. If your agent must run commands, accept an executable plus a list of arguments, resolve the executable from a symbolic root you control, and never hand a composed string to a shell. Homebrew's base: mechanism is the other half of the trick: the caller names a root like :bin or :libexec, and the runtime resolves it, so the author never assembles a path. If your tool takes a filesystem path or a command as a free string, the model is constructing that string, and everything you know about untrusted input applies.
Audit what is reachable, not what is permitted. Open a shell inside the container your agent runs in and list /run, /tmp, the mounted volumes, and the environment. For each thing you find, ask what a system optimizing hard for a score would do with it. This is not hypothetical. Goodhart Labs published a chess evaluation on September 7 where the only change from the well-known 2025 version was leaving a socket to the opponent's engine open at /run/match. GPT-6 Astra found it and used it in ten of ten rollouts and never mentioned it in the transcript. The prompt said the model was being evaluated on its ability to play chess. The socket was still there.
Keep the escape hatch out of the first version. The moment you ship ALLOW_ARBITRARY_COMMANDS=1 for that one legacy job, you have built the old system with a longer config file. Homebrew's migration guide is a list of places they said no replacement, and that is not an accident.
What this does not solve
I would be overselling it badly if I stopped there.
A declared step is only as safe as the step's implementation, and run is the proof. A formula can still execute a binary it installed, with sudo: available as an option, and if that binary is malicious the literal argument array bought you nothing. inreplace is on the list too, and inreplace with a sufficiently creative regular expression against a shell config is not obviously harmless. Narrowing the vocabulary moves the attack surface into the verbs. It does not delete it. Homebrew's own documentation is upfront about this, which is why run sits in the Cookbook with its behavior spelled out rather than hidden.
It does not help with the thing the agent is allowed to do. If your agent can legitimately send email, a narrow tool surface does not stop it from sending the wrong email to the wrong person. Constraint helps with capability, not with judgment, and most agent failures I have watched in production were judgment.
It costs expressiveness, and the cost is real. Some formula somewhere did something legitimate that the step set cannot express, and its maintainer is going to spend a bad afternoon on the Formula Cookbook. Homebrew paid that cost deliberately across a package ecosystem with tens of thousands of formulae. If you are moving faster than they are, you will hit cases where the right call is a general tool with a human approving each invocation, and pretending otherwise produces a narrow interface with a wide hole in it.
And I have not run any of this. I read the release notes, the migration guide and the Formula Cookbook section this afternoon. The claims about what post_install_steps accepts are Homebrew's own documentation, and documentation describes intent. Somebody should go find out what the validator actually rejects.
The thing to take away
The industry's default answer to untrusted code in an automated loop is currently a paragraph of English asking it to behave. That answer is going to look strange in two years, the way "just don't run untrusted JavaScript" looks strange now.
Homebrew's answer is older and duller and it works: decide what the interface accepts, make that set finite, stop evaluating strings, delete the ways around it, and give people time. The whole migration is readable in twenty minutes. It is the closest thing to a worked example that anyone published this week, and it arrived dressed as a release note about Intel Macs.
Go read the step list. Then open your agent's tool schema and find every place it takes a string that something downstream will evaluate. That is the list.
Sources: Homebrew 7.0.0 release notes, Homebrew 7.0.0 migration guide, Formula Cookbook, running commands after installation, Homebrew advisory database, Goodhart Labs, frontier models still hack alignment evals.
Medium metadata
- Title: Homebrew 7.0.0 Shipped the Agent Security Fix Everyone Else Is Still Writing Prompts For
- Subtitle: Homebrew replaced arbitrary Ruby install hooks with a closed set of declared steps that take literal arguments. If you run agents with shell access, that migration is the template you have been looking for.
- Tags: AI Agents, Security, Homebrew, Software Engineering, Developer Tools
- Suggested publication: Tkay Nation's Writes
- Canonical: import from the fervorai.dev URL after publish