Repository navigation
Releases: Exploded-views/claude-utils
Release list
v0.5.0-ev.2
This release fixes three security issues, and all users should
upgrade. First, a configuration file or other per-machine file placed
in a synced repository, including under another letter case or a
file-system alias, or a symbolic link or submodule entry in a fetched
tree, could replace local settings or be followed outside the sync
folder. Such content is now refused before anything is merged, and
local copies never follow links. Second, a repository's local git
configuration could make the tool's own git calls run a program; those
calls now override the settings that can name one, such as hooks,
fsmonitor, pagers, external diff and textconv tools, credential
helpers and ssh commands. Third, the private-only check now confirms
that the repository still belongs to the configured owner, so a
repository transferred to another account is held, at start-up and at
the running daemon's hourly recheck, instead of receiving further
pushes. Behaviour changes: config.json must be readable and writable
only by its owner (mode 0600), git hooks in a sync repository no
longer run, and git:// and plain http:// remotes are refused.
v0.5.0-ev.1
Known issue: on a host with many sync repos (seen with 36; the threshold depends on the host), the per-prompt changes hook (claude-memsync changes --hook, which the memory-notices plugin runs) can miss its 1.5 s deadline on every call. It records its progress only when it finishes, so it never catches up: each prompt waits about 1.5 s and no change notice is shown. Syncing is not affected. On such hosts, keep the memory-notices plugin disabled until the fix in the next release. To check a host, time one call: time (echo '{"session_id":"check","cwd":"/","hook_event_name":"UserPromptSubmit"}' | claude-memsync changes --hook). If it takes about 1.5 s and prints "hook deadline exceeded" to stderr, the host is affected. A call that returns at once may have found another hook running, so run it twice.
Upgrading from v0.4.0-ev.1:
- Routing now fails closed. A project folder whose name passes through a directory with more than 256 entries (on macOS, the per-user temp dir) is synced only when what lies below that directory is a single name without
-. Anything else is left unresolved rather than guessed, so some folders under very large directories now show as "ambiguous, not synced" inclaude-memsync statusinstead of syncing. To sync such a folder, pin it with afolderrule placed before thepathandremoterules inroutes.json, then restart the daemon. - A
privateOnlyrepo needs a token the daemon can use. With no token source at all, orrecheckTokenCmd: falsewith only a command, the repo is held at daemon start. A configured token source that gives no token (a failing command, an empty token file, an unset variable) now also holds the repo; inv0.4.0-ev.1such a repo started anyway. Hosts bootstrapped without--check-appand without a token file therefore hold everyprivateOnlyrepo until a check App or token file is configured (for a token file: create it, then re-run bootstrap, which writesgithubTokenFileinto eachconfig.jsononly if the file exists) and the service is restarted (claude-memsync stop, thenclaude-memsync start).claude-memsync statusshows the hold. A hold outlives switchingprivateOnlyoff; status then gives the manual release step (stop the service, delete the repo's.state/loop-failed, start it). - A folder that routes to deny now has none of its memory files mirrored, and
related suggestnever lists or opens its transcripts. - Each release now also carries four budget archives,
claude-utils-<tag>-budget-<os>-<arch>.tar.gz(linux and darwin, amd64 and arm64), covered bySHA256SUMS. Unpack the matching product archive, then run the test binaries in the budget archive withMEMSYNC_BUDGET_BIN_DIR=<product dir> MEMSYNC_BUDGET_TAG=<tag> ./claude-memsync-budget.test -test.run '^TestBudgetB' -test.v -test.timeout 900s(and the same for./route-budget.test). No Go or checkout is needed. The archive'sREADMEanddocs/releases.mdhave the details.
Changelog:
- A denied folder is fully contained: a folder that routes to deny has none of its memory files mirrored, and
related suggest(and the daemon's suggestion refresh) never lists or opens its transcripts and counts it toward no signal; tests pin both, including a deny pin listed ahead of a broader rule. - Auto maintenance no longer escapes a deadline: after a commit, fetch, merge or rebase git may start
maintenance run --auto --detach(or a detachedgc --auto), which leaves the process group via setsid and survives proc's group kill andrelated fetch's 60 s timeout. Everygitthat can start it (the sync loops' and CLI'sgitwtcalls, andrelated fetch) now carries-c gc.auto=0 -c maintenance.auto=false, and apushto a remote that is a local path orfile://URL also passes--receive-packwithreceive.autogc=false(a local transport drops-candGIT_CONFIG_*before receive-pack; a push to an ssh or https remote runs receive-pack on the server and is not ours to configure). To keep sync dirs from growing without bound, each daemon loop runsgit gc --autoitself at most once per 24 hours per sync dir (recorded in.state/maintained, so a restart does not repeat it; foreground,gc.autoDetach=false; one at a time across all loops; stopped with the daemon's context and killed with its process group at the daemon's stop or after 10 minutes; skipped while the loop is held; the changes hook never runs it). Read clones are not housekept. Upstream candidate. - The interrupt guard's
stop()waits for its goroutine:guardChildren's stop func now joins the goroutine (idempotent; it does not wait for one already exiting the process or swallowing a signal the sync flow owns), so nothing of the guard reads package state after it returns. Fixes a data race seen inTestInterruptDuringSyncFlow_SecondSignalKillsGitAndExits130under-race; harmless in production. Upstream candidate. - privateOnly needs a token the daemon can use: with no token source at all (or
recheckTokenCmd: falsewith only a command) aprivateOnlyrepo is now held at daemon start instead of starting on the init-time check. Release note: hosts bootstrapped without--check-appand without a token file now hold every privateOnly repo until a check App or token file is configured (for a token file: create it, then re-run bootstrap, which writesgithubTokenFileinto eachconfig.jsononly if the file exists) and the service restarted (claude-memsync stop, thenclaude-memsync start(they runlaunchctlon macOS andsystemctl --useron Linux));claude-memsync statusshows it. A hold also outlives switchingprivateOnlyoff: status saysheld, released by hand only: … held from an earlier runand gives the manual release step (stop the service, delete the repo's.state/loop-failed, start it). Fork-only. - Private-only check holds a repo, not the daemon: a repo whose private-only check fails at daemon start (not private, or the check cannot run, for example a GitHub App with no installation) is held: its loop does not start,
statusshowswaiting for the private-only check, re-checking with backoff, and the check is retried from 10 s, doubling to every 30 min; the loop starts, with its push guard, only after a passing check. A configured token source that gives no token (failing command, empty file) now fails the check instead of reading as "no token configured", and a push guard built then blocks pushes until the source works and the API says private; a held repo is released only by an API answer, never by a skipped check. Other repos sync meanwhile, and the daemon no longer exits, so a service manager no longer restarts it every few seconds. Upstream candidate. - Agents hear about a held repo:
changes --hookleads its output with one line per repo the private-only check holds (memsync: repo <name> is HELD by the private-only check since <time>: <reason>. …), once per session and hold, from local state files only, and also when the hook's deadline defers the change notices; text from state files is cut to one line with control and invisible format characters removed, the hold record and the per-session marker are read bounded and as regular files only (a FIFO or symlink is skipped without waiting), and the marker is written without following links. Upstream candidate. - A performance budget for the hook and the daemon, enforced by tests:
go test -tags budgetmeasures the changes hook (p95 under 200 ms over 20 runs), the daemon's start-up pass (under 10 s), idle CPU (under 1% of a core) and resident memory (under 64 MB) on a synthetic tree with a 10k-entry temp dir and 1,000 project folders, plus counted gates that load cannot hide: directory reads and bytes stay flat as unrelated directories grow, at most one of 20 overlapping hooks does the work, a hunggitnever outlives the hook's deadline, a hook with nothing new starts nogitprocess, and start-up spawns at most onegitper existing project directory. Timing gates print the load average and report INVALID above 8 instead of passing or failing. On an Apple-silicon Mac the start-up pass went from 18.7 s to 2.7 s and the hook p95 from 474 ms to 74 ms. Upstream candidate. - Resolution reads
.gitdirectly, and asksgiton any doubt: whether a directory is inside a work tree, its common dir and its HEAD come from the files (a.gitdirectory or agitdir:file withcommondir, loose and packed refs), withgitas the fallback for anything unusual (symlinked paths, submodules, bare orcore.worktreerepos, includes, reftable, unreadable or corrupt files, another owner). A directory is only ever called "not a repo" aftergitconfirms it, once per routing generation; a missing directory costs nothing. Start-up resolves folders side by side, at most 4gitchildren at a time, each with a 10 s timeout, and the daemon's loops start after at most 5 s while slow folders finish in the background. Upstream candidate. - Routing fails closed under a directory too large to list: a project folder whose name passes through a directory with more than 256 entries (on macOS, the per-user temp dir) resolves only when the part below it is a single name without
-; anything else is left unresolved, never guessed, because-may stand for/,.,_, a space or a character the folder name cannot spell, and a wrong guess would sync one project's notes into another project's repo. Such a folder is logged once and listed byclaude-memsync statusas "ambiguous, not synced" when it looks live (modified within 14 days). A path that resolves in a huge directory keeps its on-disk spelling on case-folding hosts. Behaviour change. Upstream candidate. - The changes hook runs one at a time and never leaves
gitbehind: overlapping hooks skip at once when another holds the hook lock in the first repo's state dir, and at the hook's deadline everygitit started (includin...
v0.4.0-ev.1
Upgrading from v0.3.0-ev.1: on Linux, claude-memsync install (re-run) or claude-memsync start repairs the unit. On macOS an existing launchd agent is not rewritten; run claude-memsync stop, claude-memsync uninstall, then claude-memsync install with the same --config/--routes as before, then claude-memsync start, so the agent gets RunAtLoad.
- README for the fork: the top-level README introduces the fork, credits upstream, and covers install from a release or from
integ/next, an encrypted two-machine quickstart with a join request, and Mermaid diagrams of the sync path and routing; theme-aware banners indocs/assets/;docs/fork.mdgains a section on how the fork relates to upstream, and its follow-ups say which are built oninteg/next; the release steps, version rules, local versioned builds and version resolution moved todocs/releases.md;docs/claude-memsync.mdbuilds from the fork'sinteg/next. Fork-only. - Routing on hosts that reach GitHub over port 443: with an ssh config
HostName ssh.github.com(GitHub's ssh-over-443 endpoint), an ssh-cloned checkout now gets the route keygithub.com/<owner>/<repo>instead ofssh.github.com/…, sogithub.com/…remote rules match it, as the private-only guard already treated that host. Upgrade note: aroutes.jsonrule or alias written with thessh.github.com/…host no longer matches; rewrite it asgithub.com/…. Until then a folder recorded under the old key shows a renamed-remote or changed-remote notice (or, for a client repo, is denied) instead of syncing. Upstream candidate. - Install re-run keeps unit arguments: a re-run
claude-memsync installkeeps the existing unit'sExecStartarguments and replaces only the--config/--routesgiven on that run (binary path refreshed), so a bareinstall(as the doctor/status warning suggests) can no longer drop them; a changedExecStartprints the old and new lines and says to restart;ExecStartis matched only in[Service]and CRLF units compare equal; an unreadable unit warns "cannot read the service unit"; a symlinked unit is refused beforesystemctl --user disableruns. Upstream candidate. - Autostart fix: the Linux systemd user unit is
WantedBy=default.target(kardianos/service wrotemulti-user.target, which asystemctl --usermanager never reaches, so the daemon did not start after a reboot although the unit showed "enabled");install(re-run) andstartrepair an existing unit and re-enable it (a unit counts as fine only withWantedBy=default.targetand itsdefault.target.wantslink; an unreachable user manager changes nothing and prints the manual steps), a re-runinstallwith new--config/--routesrewritesExecStart,statusanddoctorwarn about an unconfirmed one, and the macOS launchd agent setsRunAtLoad(kardianos/service defaults it to false, so inv0.3.0-ev.1the agent did not start at login). Upstream candidate. join accept|reject|list --all:join accept <label> --all --fingerprint <fp>accepts the request from<label>in every routed repo whose fingerprint matches, in one service stop and start, with one report line per repo; any mismatching request refuses every repo (exit 1), and read-only, rekey-blocked and unencrypted repos are skipped;reject --allandlist --alllikewise (#1). Upstream candidate.identity backup:claude-memsync identity backup --store-cmd "<argv>" [--verify-cmd "<argv>"] [--identity <path> | --all | --repo <name> | --config <path>] [--dry-run]stores each age identity file this host needs (its clones', the defaults related clones read with, and the classic filesrekey-pqkeeps) through your password manager (the key on the command's stdin only, never printed, logged or put in an argument or the environment), reads it back with--verify-cmdand compares SHA-256 digests, and refuses a file that is not mode 0600, yours, a single hard link, a regular file (not a symlink) and an age identity (#3). With several different keys the title must carry{fingerprint}or{name}and--verify-cmdmust differ for different keys, or it exits 2, and a final read-back reports an item a later store overwrote; it ends with anN stored (M verified, K UNVERIFIED, V verify-failed), F store-failedline, and turns core dumps off. Alsoclaude-memsync version --json(andclaude-memmerge version --json): one JSON line withname,version,commit,go,os,arch; the plainversiontext line is unchanged, any other argument exits 2, and both are documented as stable. Upstream candidate.- Faster, selectable host bootstrap (#4):
scripts/bootstrap.shtakes--project/--skip-projectto choose which checkouts get amem-project-*repo and--jobs N(default 4, max 8) to run the per-repo steps in parallel with output kept in plan order; every safety decision is a registered guard that proceeds only on positive confirmation (fault-injection tested), a half-made sync dir is moved aside after the same checks, never deleted, a missing, malformed or conflicting flag now exits 2 (usage), checked before any pause or token-file check; Ctrl-C or TERM stops the jobs and what they started, and the org token is minted once per run into a private temp dir. Fork-only. - rekey keeps the classic identity for every related clone: a symlinked clone under the related root, and a clone whose marker cannot be examined, now count as related clones that may still use the classic identity, so
recipients retire-classickeeps it (both were silently treated as not using it).
Verify every archive against SHA256SUMS before extracting; see docs/releases.md.
claude-utils fork v0.3.0-ev.1
claude-utils fork v0.3.0-ev.1
This is the first release of this fork of claude-utils. It is a pre-release, and the -ev.1 suffix keeps it clear of upstream's own version numbers.
What's in it
- Post-quantum encryption by default. New memory repos use hybrid ML-KEM-768 + X25519 age keys (
age1pq1…); classic X25519 is available only when you ask for it.recipients rekey-pqconverts an existing single-member classic repo, optionally onto one fresh root commit. It shows a dry run first, resumes when rerun, can--rollbacka migration that was not pushed, andretire-classicretires the old key. - One config directory. routes.json, apps.json and the age identity all live in
${XDG_CONFIG_HOME:-~/.config}/claude-memsyncon every OS. An identity from an earlier install (macOS~/Library/Application Support/claude-memsync/, Windows%AppData%) stays where it is and keeps working. - Host bootstrap.
scripts/bootstrap.shsets up a host in one pass, with a dry run first.- It checks prerequisites before changing anything.
- Keep-out owners and terms are optional.
- Exit codes are 0 (ok), 1 (error), 2 (usage) and 3 (paused, needs you).
- Homelab hosts without a password manager can use
--org-token-file … --delete-org-token-file. --prebuilt-dirinstalls the binaries from a release archive instead of building from source.
- macOS. Stock
/bin/bash3.2 and BSD tools are enough. A standalone binary in~/.local/binis moved aside automatically. Key and token commands wait 120 s for a password-manager prompt. - Day-to-day UX.
- Commands that need the sync lock stop and restart a running service themselves.
--repoalso accepts the GitHub repo name.join statusshows key labels.- Per-prompt hooks give up after 1.5 s and never run token commands or use the network.
- Safety. The daemon and every interactive change refuse a half-migrated repo. Filters follow the binary across upgrades. The push guard and pinned recipient review are unchanged.
- CI. There is a fork-only workflow covering vet, gofmt, race tests with per-package coverage floors that only go up, shellcheck and a portability lint, the bootstrap harness, gitleaks, a workflow lint (GitHub-hosted runners, SHA pins) and release-archive checks on Linux and macOS.
Install
Assets: claude-utils-v0.3.0-ev.1-<os>-<arch>.tar.gz (.zip on Windows) for linux, darwin and windows on amd64 and arm64, plus SHA256SUMS. Every archive holds claude-memsync, claude-memmerge and scripts/bootstrap.sh. To install, verify the archive against SHA256SUMS, extract it, and run bash <dir>/scripts/bootstrap.sh --prebuilt-dir <dir> …. See docs/releases.md and docs/setup.md.
Upgrading an existing host
- Stop the daemon, point the
~/.local/binlinks at the new build, and start the daemon again. The filters repoint themselves. - Re-run
claude-memsync doctor --install-timerso the daily timer runs unattended. - Leave an existing identity file where it is.
Known follow-ups
Fork issues #1 to #7: join accept --all, consistent repo keys across hosts, an identity backup command, faster and selectable setup for many repos, a precomputed changes index with performance budgets, and a harness wall-time budget with sharding. #6, timing-independent sync coverage, is fixed in this release.