Repository navigation
report, install and watch stop saying what was not so, and docker_top and config validate stop handing an agent a token from a container's command line or a webhook URL.
Each half has an edge. homebutler --server X docker top --json returns full command lines until that remote is upgraded, and the table docker top prints on your own terminal still shows them. config validate cannot tell a token written where the host goes from a hostname, so it still quotes one there. report cannot tell an on-demand daemon that exits once idle from one started the same way that stays up, and the first report after upgrading can still call one gone, once. A filebrowser uninstalled by an earlier release does not get its database back by itself: the reinstall warns, and the steps to recover it are below.
⚠️ Behavior changes
docker_topandhomebutler docker top --jsonreturn the kernel's name for each process incommand, not its command line (#356). 1.3.0 returned the line exactly asdocker topprinted it, so a container started with--token=…or a password as a positional argument put it in the answer, including to an agent under--max-risk read.commandnow holds ps'scomm: the executable's file name, at most 15 characters, which is the nameprocessesreports./usr/local/bin/probe serve --token=…becomesprobe. It is not read from the command line, because a process sets its own argv[0] and can put an argument there.processesleaves kernel threads out, anddocker_topstill lists every row ps returns for the container. This is a deliberate exception to docs/compatibility.md.commandis a frozen field, and its key and type stay, but what its value means narrows in this release without a deprecation period, because the old value was the disclosure. A script that read arguments out of it now gets only the name. The tablehomebutler docker topprints on your own terminal still shows the full line. On a host whose ps rejects-o pid,user,comm,args, the name falls back to the first word of the command line, with its directory removed. That can carry whatever a process put in its argv[0], but never more than that word.homebutler --server X docker top --jsonprints whatever the remote binary prints, so it keeps returning full command lines until that remote is upgraded. An MCP server on this release reduces adocker_topanswer from an older remote to the first word of each line either way- A report taken just after something started no longer calls it new the next day (#315). The one-minute rule that keeps a shell pipeline out of
reportalso kept young processes out of the saved baseline. A first report run half a minute after a container was recreated, from an ssh session that had just logged in, or from a tmux session just opened, left those processes out, and the next report named themnew. A saved snapshot now keeps young processes, with a new optional key on each entry inprocesses:youngisunconfirmedfor one not yet seen running for a minute, andrestartedfor one that had been and was young again. A process counts once a snapshot has seen it a minute old. Until then it is neithernewnorgone. That keeps the report's own ssh session, shell and| jqout of it when they are under a minute old at each report, and a job that runs for a few seconds every so often. A shell open for longer is counted, and isgoneonce it is closed. A later run of the same short job is a new instance rather than the one the baseline saw, so a run that hangs isnewonce it has been there a minute, as in 1.3.0. It also means that something never seen running for a minute that then stops is not reported. A new optional top-level key,processes_read_at, records when the process list was read. A snapshot from an earlier release has neither key, which reads as every process old, and that is what those releases kept. Going back to 1.3.0 afterwards can report the young processes a newer snapshot kept asgone, once - A daemon that restarted with the same command line within a minute of a report is no longer reported
goneand thennew(#315). It is the same process, as a restart with the same command line has always been everywhere else inreport. A restart that changed the command line is stillgone, thennewonce it has run for a minute - A daemon that keeps restarting is reported as
state <name> restarting — under a minute old at this report and the last(#315). The line appears when a process had restarted just before the last report, after having been seen running for longer, and has started again since. 1.3.0 saidgoneonce, on the first report after the loop began, and nothing after that. It says restarting rather than crashing, because the process table cannot tell the two apart. The realistic false alarm is a daemon reloaded on a schedule shortly before every report, as logrotate does, and the line is true of that too. A crash loop that changes its command line each time is a different process each time, and shows asgoneonce rather thanrestarting. One that was already looping at the first report has never been seen running for a minute, and gets no line - A daemon systemd starts on demand is no longer reported
neworgonedepending on whether it happened to be running (#363).fwupdandpackagekiton Ubuntu are started over D-Bus when something asks them a question and exit once idle, and the soak and the demo host gotgone: fwupdandnew fwupdfrom nothing but timing. On Linux,reportnow reads each process's service from/proc/<pid>/cgroupand asks about all of them in onesystemctl showcall, which gets two seconds at most. A service whose unit file isstatic, that nothing wants or requires, and that a D-Bus name or a socket starts, is on demand, and its processes get noneworgoneline. Nor do they getrestartingwhen two reports in a row each catch one just after it was started.udisks2, which has a bus name but is enabled, is still reported, as is any enabled service that stops. A service a timer or a path unit starts is not on demand, so a hungapt-dailyrun is stillnew. A different command line under the same name is stillreplaced, on the report that first sees it. Whenever the service cannot be determined, the line is kept: macOS, no systemd, a process in a user session or a container, an unreadable cgroup, or asystemctlthat fails or does not answer in time. The rule cannot tell a daemon that exits once idle from one started the same way that stays up, because systemd does not record which.polkitdis the one most hosts have: on a host where nothing requirespolkit.service, it stopping is no longer reported. A saved process carries a new optional key,on_demand: true, written only where it is true, because agonehas to be judged from the older snapshot. A snapshot from an earlier release has no such key, so an on-demand daemon its baseline caught running isgoneonce on the first report after upgrading. The--jsonreport shape is unchanged - filebrowser keeps its accounts and settings across
uninstalland reinstall (#327).uninstallsaid the data was preserved and answereddata_preserved: true, and the next install started with a new database and a new admin password. The image declares/configand/databaseas volumes, the compose file mapped only/srv, and each new container got new anonymous volumes. A new install now keeps them in~/.homebutler/apps/filebrowser/configand~/.homebutler/apps/filebrowser/database, besidedata/rather than in it, becausedata/is/srvand anything in it can be browsed and downloaded. The container now runs as the user who installed it, where it used to run as uid 1000 whatever the host's uid was. On a host whose uid is not 1000 it could not write its state to a directory homebutler created, and it can now. If you chowned~/.homebutler/apps/filebrowser/datato uid 1000 there so that uploads worked, chown it back to your own user, or filebrowser can no longer write to it. Two exceptions: under rootless Docker or Podman it runs as the container's root, which is the installing user on the host, and when homebutler runs as root (sudo) it runs as the image's own uid 1000 and the state directories are handed to that uid, rather than running filebrowser as root-
An existing install still has its database in an anonymous volume. While its container exists, the new
uninstall, or an install over a stopped container, stops it and copies/configand/databaseout into those directories before going on, and says so. A copy that fails stops it there, with the container stopped and not removed. A directory that already holds something is never overwritten. A container that was created and never ran, as a failedupleaves, does not count as the old one. A container still running is left alone until it is uninstalled. Upgrading homebutler changes nothing by itself. The volumes copied out of are kept byuninstalland removed bypurge, which lists them with the rest -
An install already uninstalled by an earlier release has no container left that names its volumes. The reinstall warns that it starts without them, and
uninstallwarns that they are not in the app directory.install_app,install_uninstalland their HTTP routes answer with the same text in a new optionalwarningfield, present only then, and the dashboard's Apps card shows it.data_preservedkeeps its meaning. The volumes are still there unless they were pruned. To bring the database back, stop the new install, find the unused volumes that hold afilebrowser.db, and copy the newest one in. Older ones can be from earlier reinstalls:homebutler install uninstall filebrowser for v in $(docker volume ls -q --filter dangling=true); do docker run --rm -v "$v":/v:ro busybox test -f /v/filebrowser.db && docker volume inspect -f '{{.CreatedAt}} {{.Name}}' "$v" done docker create --name filebrowser-recover -v <volume>:/database busybox rm ~/.homebutler/apps/filebrowser/database/filebrowser.db docker cp filebrowser-recover:/database/. ~/.homebutler/apps/filebrowser/database/ docker rm filebrowser-recover homebutler install filebrowser -
filebrowser's upstream project is archived and gets no further releases or security fixes. It stays in the catalogue, and
install listand the README say so
-
tools/listcarries MCP annotations, and sevenwritetools are marked destructive (#349, #357). A client that reads the hints can now ask before callingreport,watch_check,watch_remove,backup_create,backup_drill,install_appandinstall_uninstall, although their class is stillwrite. A single call to any of them can delete or overwrite something.reportprunes snapshots down to thekeepit is given.watch_checkprunes incidents past the retention cap.watch_removedeletes the entry.backup_createprunes archives underbackup.retention.backup_drillprunes drill records past the newest 50.install_apprewrites an existingdocker-compose.yml.install_uninstallremoves the containers, and whatever an app wrote inside them rather than to its directory. No class changed and--max-riskoffers the same tools as before- Stopping
watch startwaits for its monitors (#360, #361). On SIGTERM or SIGINT it used to printStopping all monitors.and exit at once. Now it exits once the monitors have stopped, which is normally immediate. A monitor that has not stopped after 10 seconds is left behind with a warning on stderr, and a second signal exits straight away. Under systemd this is well inside the defaultTimeoutStopSec
✨ Features
- every MCP tool now carries
readOnlyHintanddestructiveHintintools/list(#349, #357), so a client can ask before a destructive call in its own approval prompt. A read tool is read-only and not destructive. Adestructivetool is destructive. Awritetool is destructive when one call can delete or overwrite something. Under--max-risk read,reportis listed as a read, with a description that says it never saves a snapshot. The hints complement--max-riskrather than replace it: a client may ignore them, and the server still refuses anything above the ceiling.docs/mcp-server.mdsays whatdestructiveHint: falsedoes and does not mean
🐛 Fixes
- a
pswhose elapsed times could not be read made every process look as if it had just started (#315). 1.3.0 then saved no processes at all, and every report saidskipped: processes — not compared — the previous snapshot has none, which was not the reason. An unreadable time is now unknown, and a process of unknown age is compared as an old one, as a process the invocation lookup missed already was config validatequoted a Discord, Slack or generic webhook URL in full when it had no scheme, webhook token included (#358). The URL is the credential, and pasting one withouthttps://is exactly when the warning fired.config_validateis a read tool, so the token also reached an agent under--max-risk read. The warning now quotes a scheme written at the very start, if any, and the host, with…for the rest:discord.com/…,htps://discord.com/…. Auser:password@becomes…@, and anything not shaped like a host is quoted as…. A token written as the host itself still shows, since nothing tells it apart from a hostnamewatch startwarnedcontinuing without thresholds or ruleswhen only the alert rules failed to load (#342). The CPU, memory, disk and container checks readconfig.yaml'salertsblock, which had already loaded, and kept running. The warning now says the rules are skipped and those checks still run, in the wordsconfig validateuses for the same failure, and adds that the notification channels are skipped too when the failure is in a standalone alerts file- every command, including
report,statusanddoctor, waited five seconds on a terminal that does not answer a colour query (#344). The TUI library asks the terminal for its background colour as the binary starts and waits up to five seconds for an answer. On the Raspberry Pi with 1.2.0,report --no-saveunder a pty nobody answered took 6.10 s instead of 1.07 s. No homebutler style depends on the answer, so the question is no longer asked - stopping
watch startleft itsdocker eventschild running, reparented to pid 1, and every restart undernohup, tmux or a shell&added one more (#360). The process exited before the child was killed. On the Raspberry Pi, three restarts left three; now none are left. systemd setups were not affected, because the child died with the service's cgroup. Stopping while an incident was being gathered also killed the child without waiting for it, and that now goes through the same cleanup as an idle stop install_purgeover MCP andPOST /api/install/{app}/purgecan say that a volume was kept (#329). Since #326 the CLI lists a volume that still exists after a purge (external, or used by another container) as kept, and one Docker could not be asked about as unchecked, underRemoved, except:. Both other surfaces answered{status: purged, app, data_preserved: false}either way, and the dashboard's Apps card printedRemoved, with its data.after every purge, so an agent or an operator was told nothing was left while a volume still existed. The answer now carrieskept_volumesandunchecked_volumes, each a list of volume names, present only when non-empty: a purge that removed everything answers exactly as before. The Apps card lists them underRemoved, except:, as the CLI does. These are new optional fields onInstallResult, which 1.x allows.data_preservedis unchanged: it names the operation,truefor uninstall andfalsefor purge, not the outcome, and the two new fields are what say what is actually left
📚 Documentation
docker_topwas described as read-only while it returned every command line in a container in full, andconfig_validatewas not mentioned at all, though it names the config file, servers, endpoints and backup directory (#355). Thereadclass says what a call does to the machine, and several places used it as if it also said what the call discloses. The README,docs/mcp-server.md,docs/compatibility.mdand the agent skill now say what a read tool returns, not only that it changes nothing.docs/mcp-server.mdalso saiddocker_inspectreturns configuration that can hold a credential: it never reads environment values, and what it does return is host mount paths and container addresses