Skip to content

caskroom: filter out casks without valid installation metadata#21630

Merged
MikeMcQuaid merged 1 commit intoHomebrew:mainfrom
koddsson:fix-caskroom-casks-installed-filter
Feb 26, 2026
Merged

caskroom: filter out casks without valid installation metadata#21630
MikeMcQuaid merged 1 commit intoHomebrew:mainfrom
koddsson:fix-caskroom-casks-installed-filter

Conversation

@koddsson
Copy link
Contributor

Summary

  • Caskroom.casks now filters results with .select(&:installed?) to exclude cask directories that lack valid .metadata, preventing brew info --installed --json=v2 from returning casks with installed: null
  • Updated installed_dependents_spec.rb test to properly create .metadata directory structure so test casks are recognized as installed

Motivation

When a cask directory exists in the Caskroom (e.g. /opt/homebrew/Caskroom/arq/) but its .metadata directory is missing or corrupt, Caskroom.casks still includes it. This causes brew info --installed --json=v2 to output casks with "installed": null, which breaks tooling that parses the JSON output.

The fix adds .select(&:installed?) after the filter_map block in Caskroom.casks, which checks for a valid installed_caskfile — the same condition installed_version relies on. This matches the method's documented purpose of "Get all installed casks".

Test plan

  • brew typecheck passes
  • brew style --fix Library/Homebrew/cask/caskroom.rb Library/Homebrew/test/installed_dependents_spec.rb — no offenses
  • brew tests --only=installed_dependents — all 13 specs pass
  • brew tests --only=cmd/--caskroom — all specs pass

🤖 Generated with Claude Code

`Caskroom.casks` enumerates all directories in the Caskroom path but
doesn't verify each cask is actually installed. When a cask directory
exists but its `.metadata` directory is missing or corrupt,
`installed_version` returns `nil`, causing `brew info --installed
--json=v2` to include casks with `installed: null`.

Add `.select(&:installed?)` to filter out these ghost entries so the
method only returns genuinely installed casks, matching its documented
behaviour of "Get all installed casks".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Member

@MikeMcQuaid MikeMcQuaid left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @koddsson! Not sure yet this is the ideal fix. What is the expected/necessary installation metadata that's missing in this case? Would be nice if we can infer it as it does seem in this case that the cask is still actually installed, we just can't detect it properly.

Similarly: any idea how casks end up in this state?

@koddsson
Copy link
Contributor Author

(I used a LLM to help me explore the code and keep the thoughts and ideas in order as well as helping me convey my thoughts in the most concise way)

What metadata is missing?

The expected on-disk structure for a properly installed cask is:

  /opt/homebrew/Caskroom//
  ├── /                              # version dir (artifacts)
  └── .metadata/
      └── /
          └── /                    # e.g. 20240101120000.000
              └── Casks/
                  └── .rb (or .json)   # the installed caskfile

The installed? check calls installed_caskfile, which calls timestamped_versions — this globs for
.metadata/<version>/<timestamp> directories. If the .metadata tree is missing or incomplete (no version subdir, no
timestamp subdir, or no caskfile inside), then timestamped_versions returns [], installed_caskfile returns nil,
and consequently:

Notably, Homebrew itself already treats these casks as not installed — running brew upgrade or brew uninstall on them errors with Cask 'X' is not installed. This fix makes Caskroom.casks consistent with that.

How do casks end up in this state?

Honestly, I'm not fully sure. I've got roughly 80 affected casks across dozens of devices in my fleet, which suggests something more systemic than a one-off interrupted install. Some possibilities:

  • Permission issues — If the .metadata directory isn't readable by the brew process, the glob in timestamped_versions returns empty even though files exist.

  • Interrupted install/uninstall — During stage, the Caskroom directory is created and artifacts are extracted before save_caskfile writes the .metadata tree. A crash between those steps leaves a bare directory. Though this seems unlikely.

  • Something Workbrew-specific — I'm discovering this issue at my day job at Workbrew. It's worth investigating whether the Workbrew agent or MDM tooling could be creating Caskroom directories without full metadata.

This is worth investigating further separately, but regardless of the root cause, these casks are in a state where Homebrew already considers them not installed.

Can we infer the metadata instead?

In principle, yes — we could infer the version from the version subdirectory name (e.g. Caskroom/firefox/1.0/ → version 1.0) and write the current tap caskfile into a new .metadata tree. But that would effectively be auto-adopting casks we're not confident about: the current tap caskfile may not match what was originally installed, and we'd be fabricating an install timestamp. It feels like that could cause its own problems.

I think the safer approach is to filter now (making Caskroom.casks match its documented "Get all installed casks" behaviour and match what the rest of Homebrew already thinks), and consider metadata inference/repair as a potential follow-up if we determine the root cause warrants it.

I'm happy to be told I'm wrong on this inference if there's stuff about it that I'm missing. Maybe it's a lot easier than I think 😄

Reproduction

I reproduced this locally by creating a bare Caskroom directory for a real cask token (e.g. mkdir -p /opt/homebrew/Caskroom/firefox/1.0) without a .metadata tree. Before the fix, brew info --installed --json=v2 includes it with "installed": null. With the .select(&:installed?) filter, it's correctly excluded.

One of the Workbrew engineers has this issue on their Device and I'm planning on jumping on a call with them later today and take a look together at their Caskroom to see if it's a permissions issue or something different. I'll make sure to report back here with our findings.

@MikeMcQuaid
Copy link
Member

Notably, Homebrew itself already treats these casks as not installed — running brew upgrade or brew uninstall on them errors with Cask 'X' is not installed. This fix makes Caskroom.casks consistent with that.
This is worth investigating further separately, but regardless of the root cause, these casks are in a state where Homebrew already considers them not installed.

Good enough for me, thanks!

@MikeMcQuaid MikeMcQuaid added this pull request to the merge queue Feb 26, 2026
Merged via the queue into Homebrew:main with commit f0057b6 Feb 26, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants