brew audit은 검사였지만 읽기 전용은 아니었다

Homebrew formula를 검사하고 싶었다.

명령 이름도 딱 맞았다.

brew audit ...

감사한다.

살펴본다.

문제가 있으면 알려준다.

그 정도로 읽었다.

실제로는 개발자 모드를 켜고 Homebrew의 개발용 gem bundle을 설치했다.

그날 이후 brew list도, brew info도, brew outdated도, brew config도 깨졌다.

감사를 요청했더니 회계 시스템이 Ruby 의존성까지 인수인계한 셈이었다.

이름은 권한 모델이 아니다

audit, check, doctor, inspect 같은 이름은 마음을 편하게 한다.

대개 읽기 전용처럼 들린다.

하지만 CLI 이름은 동사가 아니라 인터페이스다.

실제 권한은 구현이 결정한다.

현재 Homebrew의 audit는 일반 명령 디렉터리가 아니라 dev-cmd 아래에 있다.

Homebrew의 시작 스크립트는 developer command를 처음 실행한 사용자를 감지하면 설정을 기록한다.

현재 소스의 동작을 줄이면 이렇다.

developer command 감지
→ homebrew.devcmdrun 설정 기록
→ developer-command 환경 활성화
→ audit 구현 진입

검사 대상을 읽기 전에 이미 로컬 상태 하나가 바뀐다.

이것은 숨은 버그라기보다 도구의 운영 모델이다.

문제는 내가 그 모델을 읽지 않고 명령 이름만 읽었다는 데 있었다.

audit는 필요한 gem을 준비한다

현재 audit.rb는 audit와 ast gem group을 구성한다.

style 검사를 함께 수행하는 경우 style group도 더한다.

그다음 호출은 이름부터 정직하다.

Homebrew.install_bundler_gems!(groups: gem_groups)

install_bundler_gems!는 단순한 load-path 설정 함수가 아니다.

현재 gem_setup.rb는 bundle 상태를 확인하고 필요하면 bundle install을 실행한다.

설치가 필요하지 않아도 bundle clean을 실행할 수 있다.

마지막에는 사용한 gem group 설정을 기록한다.

즉 brew audit의 더 정확한 정신 모델은 이쪽이다.

필요한 개발 환경을 준비한 뒤 formula와 cask를 검사한다.

“현재 환경을 한 글자도 바꾸지 않고 파일만 읽는다”가 아니다.

실제로 무엇이 깨졌나

2026년 8월 15일 이 Mac에서는 audit 실행 뒤 vendored bundle에 더 새로운 JSON gem이 들어왔다.

그 gem은 portable Ruby가 이미 제공하던 JSON 사본과 충돌했다.

결과는 연쇄적이었다.

brew list      → 실패
brew info      → 실패
brew outdated  → 실패
brew config    → 실패

개발 gem을 다시 맞추는 명령도 같은 충돌을 밟아 실패했다.

문제를 고쳐야 할 도구가 문제와 같은 Ruby 환경에서 실행되니 자가 수리가 막혔다.

이 실패 형태가 모든 Homebrew 설치에서 재현된다는 뜻은 아니다.

특정 날짜의 이 설치에서, 특정 vendored gem 조합이 충돌했다는 관찰이다.

하지만 더 작은 결론은 설치마다 반복할 필요가 없다.

brew audit는 상태 변경 가능성이 있는 개발 명령이다.

그 사실은 현재 소스에도 남아 있다.

공식 문서가 틀렸다는 이야기는 아니다

Homebrew의 Formula Cookbook은 신규 formula를 제출하기 전에 brew audit를 실행하라고 안내한다.

공식 manpage도 audit를 developer command로 분류한다.

Homebrew 본체나 공식 tap에 기여한다면 audit는 필요한 검증이다.

그 검증에는 필요한 개발 환경을 준비할 권한도 따라온다.

문서와 구현은 서로 모순되지 않는다.

내 기대가 달랐다.

나는 개인 tap의 formula 하나를 로컬 일상 환경에서 읽기 전용으로 검사한다고 생각했다.

Homebrew는 개발자 워크플로를 시작한다고 해석했다.

같은 명령이 서로 다른 계약으로 읽힌 것이다.

필요한 검사는 격리해서 실행한다

공식 제출 때문에 audit가 필요하다면 답은 audit를 영원히 피하는 것이 아니다.

일상적으로 쓰는 패키지 관리자 설치와 검증 환경을 분리하는 것이다.

예를 들면 다음처럼 경계를 세울 수 있다.

일상 Homebrew 설치 → 앱과 도구를 실제로 사용하는 환경
전용 CI 또는 disposable 환경 → developer command와 audit를 수행하는 환경

CI가 적합한 이유는 단순히 깨져도 버리기 쉬워서가 아니다.

개발용 gem 설치와 설정 변경이 그 환경의 목적 안에 들어오기 때문이다.

권한과 기대가 맞는다.

로컬에서 꼭 실행해야 한다면 먼저 변경 대상을 확인해야 한다.

  • developer mode 설정을 기록하는가;
  • 별도 gem이나 package를 설치하는가;
  • cache나 vendor directory를 정리하는가;
  • auto-update 정책이 달라지는가;
  • 문제가 생겼을 때 정상 명령과 다른 복구 경로가 있는가.

“검사 결과가 무엇인가”보다 “검사를 준비하며 무엇을 바꾸는가”를 먼저 묻는 셈이다.

style은 더 좁지만 마법의 read-only 명령은 아니다

이 사건 다음 날에는 명시한 formula 경로만 brew style로 검사했다.

그 실행은 style 위반을 보고했고, 이후 brew list, brew info, brew --version은 정상 동작했다.

새 JSON gem도 생기지 않았다.

이 설치에서는 필요한 linter로 쓸 수 있다는 관찰을 얻었다.

다만 그 결과를 “brew style은 어디서나 순수한 읽기 전용 명령”으로 확대하면 같은 실수를 반복한다.

현재 style.rb도 developer command이고 style gem group을 install_bundler_gems!에 넘긴다.

이미 필요한 bundle이 준비된 이 기기에서 추가 변경이 없었던 것과, 구현상 변경 가능성이 없는 것은 다른 주장이다.

그래서 이 기기의 운영 규칙은 구체적이다.

formula lint → 명시 경로의 brew style
사용자 경로 검증 → brew install + brew test
brew audit → 이 일상 설치에서는 실행 금지
공식 audit 필요 → 격리된 환경으로 이동

도구를 선과 악으로 나누지 않는다.

목적과 격리를 나눈다.

install과 test는 상태를 바꾼다고 솔직하게 말한다

개인 tap의 배포 전 검증에서 중요한 질문은 사용자가 formula를 설치하고 실행할 수 있느냐였다.

그 속성을 읽는 가장 직접적인 경로는 tap 이름을 통한 설치와 formula의 test block이다.

brew install owner/tap/formula
brew test formula

물론 이것도 상태를 바꾼다.

formula를 설치하는 명령이니까 당연하다.

차이는 변경이 숨어 있지 않다는 데 있다.

검증하려는 속성과 발생하는 변경이 같은 방향이다.

설치는 다운로드, checksum, 설치 단계를 읽는다.

test는 설치된 산출물의 기본 동작을 읽는다.

lint는 formula의 스타일을 읽는다.

audit는 Homebrew 기여 규칙과 추가 검사를 읽지만, 그전에 개발 환경도 준비한다.

한 단어로 “검증”이라고 묶으면 이 차이가 사라진다.

복구는 설치 시각부터 읽었다

충돌이 생긴 뒤에는 gem 이름만 보고 지우지 않았다.

portable Ruby의 기존 사본과 audit가 추가한 사본이 함께 있었기 때문이다.

먼저 파일 수정 시각으로 사건 때 생긴 directory 집합을 식별했다.

그다음 offending gem directory를 바로 삭제하지 않고 옆으로 이동하는 복구안을 만들었다.

여기서도 reader가 중요했다.

gem 이름만 읽기 → 같은 이름 계열을 너무 넓게 잡을 수 있음
mtime과 설치 위치 읽기 → 사건이 만든 후보 집합을 좁힘
이동 후 brew 명령 재검증 → 복구 경계를 확인함

이 글은 그 삭제·이동 명령을 복구 레시피로 제공하지 않는다.

Homebrew prefix와 gem 구성은 설치마다 다르고, 잘못 지우면 손상이 커진다.

핵심은 파괴적 복구 전에 사건이 만든 대상을 다시 식별한다는 원칙이다.

check라는 이름을 볼 때 확인할 것

새로운 검사 명령을 만나면 도움말만 읽고 끝내지 않는다.

가능하면 구현이나 공식 운영 문서에서 다음을 찾는다.

설정 파일 write
package 또는 gem install
cache clean
auto-update
generated artifact
network request
developer mode 전환

그중 하나라도 있으면 read-only 진단이 아니라 상태를 가진 워크플로다.

실행 위치를 선택해야 한다.

일상 환경인가.

throwaway 환경인가.

CI인가.

복구 가능한 snapshot이 있는가.

명령 이름은 이 질문에 답하지 않는다.

실전 규칙

audit라는 이름을 read-only 권한으로 번역하지 않는다.

현재 구현이 설정, dependency, cache를 어떻게 다루는지 먼저 읽는다.

Homebrew의 audit는 developer command이며 필요한 gem bundle을 준비한다.

공식 제출에 audit가 필요하면 그 변경이 허용된 격리 환경에서 실행한다.

개인 formula의 사용자 경로는 설치와 test로 직접 검증한다.

lint가 필요하면 명시 경로의 style을 사용하되, 그것도 구현상 developer command임을 기억한다.

이 Mac에서는 과거 충돌 근거 때문에 brew audit를 다시 실행하지 않는다.

복구가 필요하면 사건 직전·직후의 mtime과 경로로 대상을 좁힌 뒤 움직인다.

명령은 이름으로 권한을 선언하지 않는다.

부작용 목록이 진짜 권한표다.