Repository navigation
Update
Already using BenchBar? In the app, click Update Now in the update banner or the menu bar menu (BenchBar 0.6 and later check once a day). On 0.5.x, run this in Terminal:
curl -fsSL https://raw.githubusercontent.com/askysh/benchbar/main/install.sh | bash -s -- --yesIt updates the benchbar CLI in ~/.local/share/benchbar and the app in ~/Applications, quits a running BenchBar first and never touches a bench. From 0.6 on, benchbar self-update runs the same. If your benchbar is a git checkout of your own, git pull there and add --app-only.
Installed with Homebrew? Run brew upgrade, or click Update Now in the app: from 0.7.1 the app carries its command line tool, and Homebrew's benchbar hands off to it, so either way updates both. The tap gets a release a few minutes after it is published.
Changes
A fix pass from a full review: benchbar stops only what it can prove is a
bench's own, fails closed on every write, moves v15-lts to node@22 before
Homebrew disables node@20 on 2026-10-28, installs cleanly on a fresh
Homebrew MariaDB and next to other benches, and hardens the MCP server.
Run benchbar repair once after updating: the runner and the helper block
are rewritten.
Fixed
- The runner's pre-start cleanup stops only listeners whose working
folder is this bench; another bench's Redis or web server and any
other program on the same ports are left alone. When such a process
still holds a port, the runner pauses with the new stop reason
port_conflictinstead of starting honcho into ports it cannot bind
(runbenchbar doctor). benchbar downno longer signals a process whose working folder lsof
cannot read; doctor's orphans check names such a process and says why
it is not stopped, andports plancounts it as a conflict, asup
already did.benchbar upon a bench whose only live process is a leftover Redis
(a site add cut short) no longer reports "already running": it starts
the bench, and the runner clears that Redis first.benchbar adopt,repairandserviceon a bench running under
bench startorbenchfgleave its processes alone: the agent loads
stopped and benchup takes over once that session ends.benchbar site dropnever drops a bench's only site, also when
SITE_NAMEor--sitenames another site, and treats the site in
currentsite.txtas the default too.- The repair plan says when a MariaDB step restarts the server shared by
every bench, and the restart names the running benches first. - The MCP tools that change a bench (
benchbar_up,benchbar_down,
benchbar_restart,benchbar_app_add) refuse a folder that is not in
benchbar_list. benchbar fgtakes the CLI lock while it stops the background bench,
so it cannot cut a running site add short, and refuses to start when
the stop fails.- A fresh Homebrew MariaDB (10.4 and newer) gives root a socket login and
no password;benchbar installread that as "root already has a
password" and stopped with exit 2. It now secures root through the
macOS user's socket account, as the Homebrew install leaves it, and
continues. A server that accepts neither login still stops and asks. - Cancelling
benchbar installat "Proceed?" (or running it without a
terminal and without--yes) ends the run with exit 1 and "Cancelled.
No bench or site was created"; it no longer reported the phase as done
and loaded a launchd agent for a bench that was never created. site add,site dropand the install stop with "MariaDB is not
running" when it is not, instead of blaming the root password.- A stale
MARIADB_ROOT_PASSWORDin the environment no longer makes the
site step fail when the Keychain holds the working password: it is
verified first and the Keychain is tried next, as phase 00 already did. OFFLINE=1andBENCHBAR_OFFLINE=1reach both phase scripts; before,
they reset it and ran the remote branch checks anyway.benchbar installasks for sudo up front only for steps that will run:
no prompt for the wkhtmltopdf package when Rosetta is missing and
nothing can agree to install it, or when the package would be skipped.- The message after a stopped install says what phase 00 did ("System
dependencies were set up as far as possible; no bench or site was
created") instead of "Nothing else changed". - A command inside the run that fails with code 2 now ends the run with
1, so exit 2 keeps meaning that phase 00 or 01 stopped for a manual
step (most often the MariaDB root password). - A launchd agent that is on launchd's disabled list (after
launchctl disableor an oldlaunchctl remove) is enabled before the bootstrap
is retried, instead of failing three times. - A bench whose Frappe no profile knows (develop, v17, v14) with no stored
profile no longer falls back tov15-ltssilently: doctor's and
repair's header say the profile is only the default, the env checks say
so and warn instead of offering a rebuild, andrepairrefuses
to rebuild the env with a guessed Python.benchbar install --profile NAMEsets the profile. bench versionfailures are classified before any fix is offered: a
missing or brokenbenchcommand getsuv tool install frappe-bench
(or the pipx equivalent), an app that does not import is named with
bench setup requirements --python, and only a failure inside the env
itself (frappe or a library that does not import) still leads to an env
rebuild. None of the classified cases moves the env aside.benchbar repairrefuses to rebuild the env while the bench is running
(its processes run from that env) and says to stop it first.- The installer checks that
benchruns (bench --version) before using
it, names the reinstall command for its owner when it does not, and
warns when it is older than the profile's known minimum (bench_minin
config/release-profiles.tsv). - Long and timed commands (bench init, pip, yarn, git, brew) run in their
own process group, and a timeout, a Ctrl+C or a TERM to benchbar stops
the whole group before the run ends and the lock is released; before,
only the top process was signalled and the rest kept writing into the
bench. The MCP server does the same for a tool call that runs out of
time, so a setup Redis never outlives it. - A long command that stops to read the terminal it does not have (a
password prompt insidebench get-apporbrew) is ended with a clear
message instead of waiting for ever behind the spinner. - The run lock reclaims a stale lock by renaming it away, so several runs
that find one at once leave exactly one holder, and a lock whose pid is
not written yet is left alone for 5 seconds instead of being stolen. - Every mutating run also takes the bench's own lock
(<bench>/.benchbar.lock), so a Homebrew CLI, the app's CLI and a git
checkout working on one bench exclude each other. benchbar upandrestartrelease the locks once the agent is kicked,
before the wait for the site, so other benches' commands are not refused
for 45 seconds.- Writers of a state file build the new file under a private temp name and
a per file lock, so a status poll and a repair no longer lose each
other's keys. app focuswithout an app name (the listing) takes no lock;
self-updateandregistertake it and write a run log.- Log files and backup folders carry the run's pid in their name
(<date>-<time>-<pid>), so two runs in the same second never share one. - Every write helper fails closed: a backup, temp file, awk or rename
that fails leaves the target untouched and the step is reported as
failed, not done. A read only folder or a full disk no longer shows a
green repair. - The shell rc block is written into the real file behind a symlinked
~/.zshrc(the link stays a link, the file keeps its mode), every stage
is checked so a failed rewrite leaves the file byte-identical, and
$ZDOTDIR/.zshrcis used when ZDOTDIR is set; the one line installer
does the same for its PATH block. benchbar repairunder--yesor without a terminal no longer reports
the "stop Homebrew redis" question as done: a question nobody can
answer is left out of the plan and listed as optional, so a second
repair saysunchanged. A declined hosts entry isskipped, and its
fix adds the line inside the benchbar block.- Large logs are rotated by copy and truncate, so the bench keeps writing
to the live file (a rename took the writers along and left the live
file empty), and at most three.oldcopies are kept. - An interrupted
bench new-siteis remembered (a marker in the bench's
logs/.benchbar); the next run names the incomplete site and how to
move it aside instead of taking it for created. benchbar lock applyfails when a fresh clone cannot reach its pinned
commit (it reported done), leaves that clone out of the build, and
skips an app whose detached HEAD holds commits no branch has instead
of orphaning them.site dropmoves the default site only afterdrop-sitesucceeded; a
failed drop leaves the default where it was.- The runner treats honcho ending with code 0 without a stop request as a
crash (a bench process ended, for example redis on a stray TERM), so
launchd restarts it under the crash guard instead of leaving the bench
silently down; and its crash guard fails closed when the start history
or the stop flag cannot be written. - The legacy agent migration and
uninstall-servicekeep the plist when
launchd does not let go of the job, and report a move that failed. - A bench folder that cannot be written (read only) stops a run at once
with "Could not create the lock ... (read only?)", instead of five
retries that blamed another benchbar run. benchbar logs --process worker(andfile: workerin the MCP
benchbar_logs_tail) always returned no lines:Procfile.leansends the worker to
logs/worker.log, not tobench.log. It now reads that file
(--previouswith it is refused: there is no previous worker log).--jsonoutput removes a terminal color code as a whole; before, the
escaping dropped only its ESC byte and left[31min log lines and
messages.- An MCP action whose follow up status failed or hung lost the action's
result; the result now comes back withafter_error. Invalid UTF-8 on
stdin no longer ends the server. - The one line installer's state (default bench, port claims, per bench
settings in~/.local/share/benchbar/.benchbar) now moves to
~/.local/state/benchbareven when BenchBar.app made its
bin/benchbarlink there before the first CLI run; before, the new
folder counted as moved and the old state was left behind. The move is
serialized with an atomic guard, so first runs started at once (the
app's polls) leave one state folder, a guard left by a killed run is
reclaimed, and a run cut short at any step leaves a layout the next
run completes (doctor's Second CLI check says so meanwhile). - The helper block (
benchupand friends) written by the app's CLI names
Homebrew's or the installer'sbenchbarwhen one hands off to it, and
the app's link only when nothing else is installed: the link goes with
the app, that copy keeps running on its own. - Doctor's Second CLI check under the app's CLI reports a second state
folder in~/.local/share/benchbarnext to the user state (never a
delete: the Trash is offered) and a Homebrew or installer copy newer
than the app's CLI, with the app update as the fix. benchbar whereshows the version of the copy that handed off
(handoff_from_versionin the JSON) and the folder behind the state
path when it is a link (state_dir_real,state LINK -> REAL).install.sh --uninstallkeeps the checkout when
~/.local/state/benchbaris a link into it (the state on another
volume), and says to move the state first.- Docs:
status,listanddoctor --jsonare read only except for the
one time move of the state folder, which renames and links. benchbar installof a second bench while another bench or program
holds the default ports picks the next free port block (or the one
--port-offsetnames) and writes it before phase 01 starts the bench's
Redis; it stopped with "Port 11000 ... is held by another process".- A fresh v15 install puts
setuptools<70into the new env, as repair
does, so the firstbenchbar doctorno longer warns about
pkg_resources. - Doctor's stop flag check names the process that holds the bench's ports
after aport_conflictpause, or says the ports are free now; it
pointed at the orphans check, which only lists the bench's own
processes.
Changed
- The runner (template v7) and the helper block (v2) changed;
benchbar doctormarks existing ones outdated once andbenchbar repair
re-renders them. repairsays when a check still fails after the run and has no action of its own (a disabled formula, a missing tool);doctorexits 1 while it does.- The default profile
v15-ltsinstallsnode@22instead ofnode@20,
which Homebrew disables on 2026-10-28 (Frappe v15 needs Node 18 or
newer). A bench set up withnode@20keeps working;benchbar doctor
flags its shell block, agent, Node and the missing formula, andbenchbar repairinstalls
node@22, puts yarn under it and re-renders both PATHs.node@20is
not removed. benchbar installdoes its twosudosteps (the wkhtmltopdf package,
the/etc/hostsline) first and drops the credential withsudo -k
before Homebrew, pip, npm, yarn orbench get-apprun; it still asks
once, up front.00-mac-system-deps.shrun on its own drops it right
after the package step.- The wkhtmltopdf package is copied into a root owned folder under
/tmp,
hashed there as root and installed from that copy, so the file that was
verified is the file that is installed.--dry-runprints those steps. MARIADB_ROOT_PASSWORDandADMIN_PASSWORDfrom the environment reach
onlybenchbarand the two phase scripts; no other child process (brew,
pip, npm, yarn, bench) sees them.--yes,--make-defaultand an approved port plan are flags only: a
FL_ASSUME_YES,FL_MAKE_DEFAULTorFL_PORT_PLAN_APPROVEDleft in
the environment no longer answers a question.benchbar mariadb-passwordwithout a terminal says that--yesis needed instead
of printing the password.- The Keychain item for the MariaDB root password is written through
security -i(the password is never a command line argument) and names
/usr/bin/securityas its one trusted application. SECURITY.md says what
that means and how to make every read ask. benchbar reportalso masks secrets that have no key in front of them:
user:password@inside URLs (redis, git with a token), JWTs, GitHub,
Slack, OpenAI and AWS token shapes, and email addresses (<email>);
pwdandpasswdkeys and Python keyword arguments (password='...')
count as credential keys.REDACTIONS.txtlists each kind.benchbar logs --jsonredacts the lines with the report's rules and
caps-nat 2000 lines, saying so with"truncated_to":2000.
benchbar_logs_tailover MCP redacts the same way and refuseslines
outside 1 to 2000.benchbar logswithout--jsonis still the file as
it is.- A git URL with a token in it no longer reaches the run log, the "Last
command failed" line, a failed command's last 40 lines or the echo of
an env-provided answer: theuser:token@part is written as***@. adopt,install,service,repair,up,restart,autostart,
ports apply,site addandsite defaultrefuse a bench path (and a default site name) with a double quote,
backslash,$, backtick,<,>,&or a control character before
writing anything, and say which character is in the way; spaces and
apostrophes stay fine.doctorstays read only and reports it as the new
bench_pathcheck. Template values are also escaped for bash and XML as
a second guard.- The
/etc/hostsrewrite (a line inside benchbar's block, or removing
one onsite drop) happens on the root side: awk as root writes
/etc/hosts.benchbar.newthroughsudo tee, the result is checked
(every line an address and names, exactly one line more or fewer) and
moved into place; anything else leaves the file untouched. A site folder
whose name is not a valid site name is skipped with a warning and never
reaches sudo. - The state store (
state.env, the per bench files) decodes its values
withouteval: a hand editedKEY=$(...)line is read as text, runs
nothing and expands no glob. - The MCP server runs every tool call on its own thread, so a status
answers while an app add runs; actions run one at a time; a
notifications/cancelledstops the running CLI and its process group
(a cancel during the status that follows an action still returns the
action's result, withafter_error).
Arguments are checked against the schema and a bad value is a tool
error that names the rule. Log lines, git output,hooks.py, profile
files and action output come back with a first line that labels them
as data from the bench, not instructions, with terminal colors removed
and a 200 kB cap (BENCHBAR_MCP_MAX_BYTES).tools/listand
tools/callbeforeinitializeare refused (-32002). Each tool
says whether it is read only, destructive, idempotent or reads the
network, and the app add token is described as what it is: proof that
the bench has not changed since the plan. benchbar mcpruns on/usr/bin/python3 -Iwhen the Command Line
Tools are installed, elsepython3fromPATH, and refuses anything
older than 3.9 with the versions it found; a GUI client'sPATHno
longer picks the interpreter.
Added
benchbar doctor --jsonsays where the profile came from
(profile_source), and the app's Health page says when the profile is
only the default for a bench no profile matches.- BenchBar.app knows the
port_conflictstop reason: "Paused: a port is in
use" with a link to Health, instead of "Paused after repeated crashes". - New doctor check
env_setuptools: on Frappe v15 the env must import
pkg_resources(setuptools 70 and later dropped it);repairinstalls
setuptools<70into the env, and an env rebuild on v15 does the same. - New doctor check "Formula lifecycle": a WARN when Homebrew disables one
of the profile's Python, Node or MariaDB formulae within 90 days, a
FAIL once it has or once brew no longer knows the formula.
scripts/check-profile-formulae.shruns the same
check over every profile and stops a release inrelease-local.sh. benchbar install --offlineskips the remote checks of both phase
scripts.benchbar mcphas a read onlybenchbar_app_listtool (app list --json --no-sites), andbenchbar_logs_tailtakesfile:bench,
worker(logs/worker.log),worker_errororprevious.benchbar site list --jsoncarries each site'sdb_nameand the
MariaDBdb_port(never the password).
Signed with a Developer ID and notarized by Apple. Install with Homebrew:
brew install askysh/tap/benchbar askysh/tap/benchbar-appor with the DMG, or with the one line installer:
curl -fsSL https://raw.githubusercontent.com/askysh/benchbar/main/install.sh | bashChecksums are in SHA256SUMS.