Skip to content

Releases: Exploded-views/claude-utils

v0.5.0-ev.2

v0.5.0-ev.2 Pre-release
Pre-release

Choose a tag to compare

@gitgat gitgat released this 01 Nov 21:40

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

v0.5.0-ev.1 Pre-release
Pre-release

Choose a tag to compare

@gitgat gitgat released this 30 Oct 04:59

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" in claude-memsync status instead of syncing. To sync such a folder, pin it with a folder rule placed before the path and remote rules in routes.json, then restart the daemon.
  • A privateOnly repo needs a token the daemon can use. With no token source at all, or recheckTokenCmd: false with 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; in v0.4.0-ev.1 such a repo started anyway. Hosts bootstrapped without --check-app and without a token file therefore hold every privateOnly repo until a check App or token file is configured (for a token file: create it, then re-run bootstrap, which writes githubTokenFile into each config.json only if the file exists) and the service is restarted (claude-memsync stop, then claude-memsync start). claude-memsync status shows the hold. A hold outlives switching privateOnly off; 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 suggest never 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 by SHA256SUMS. Unpack the matching product archive, then run the test binaries in the budget archive with MEMSYNC_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's README and docs/releases.md have 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 detached gc --auto), which leaves the process group via setsid and survives proc's group kill and related fetch's 60 s timeout. Every git that can start it (the sync loops' and CLI's gitwt calls, and related fetch) now carries -c gc.auto=0 -c maintenance.auto=false, and a push to a remote that is a local path or file:// URL also passes --receive-pack with receive.autogc=false (a local transport drops -c and GIT_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 runs git gc --auto itself 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 in TestInterruptDuringSyncFlow_SecondSignalKillsGitAndExits130 under -race; harmless in production. Upstream candidate.
  • privateOnly needs a token the daemon can use: with no token source at all (or recheckTokenCmd: false with only a command) a privateOnly repo is now held at daemon start instead of starting on the init-time check. Release note: hosts bootstrapped without --check-app and 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 writes githubTokenFile into each config.json only if the file exists) and the service restarted (claude-memsync stop, then claude-memsync start (they run launchctl on macOS and systemctl --user on Linux)); claude-memsync status shows it. A hold also outlives switching privateOnly off: status says held, released by hand only: … held from an earlier run and 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, status shows waiting 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 --hook leads 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 budget measures 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 hung git never outlives the hook's deadline, a hook with nothing new starts no git process, and start-up spawns at most one git per 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 .git directly, and asks git on any doubt: whether a directory is inside a work tree, its common dir and its HEAD come from the files (a .git directory or a gitdir: file with commondir, loose and packed refs), with git as the fallback for anything unusual (symlinked paths, submodules, bare or core.worktree repos, includes, reftable, unreadable or corrupt files, another owner). A directory is only ever called "not a repo" after git confirms it, once per routing generation; a missing directory costs nothing. Start-up resolves folders side by side, at most 4 git children 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 by claude-memsync status as "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 git behind: overlapping hooks skip at once when another holds the hook lock in the first repo's state dir, and at the hook's deadline every git it started (includin...
Read more

v0.4.0-ev.1

v0.4.0-ev.1 Pre-release
Pre-release

Choose a tag to compare

@gitgat gitgat released this 27 Oct 23:16

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 in docs/assets/; docs/fork.md gains a section on how the fork relates to upstream, and its follow-ups say which are built on integ/next; the release steps, version rules, local versioned builds and version resolution moved to docs/releases.md; docs/claude-memsync.md builds from the fork's integ/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 key github.com/<owner>/<repo> instead of ssh.github.com/…, so github.com/… remote rules match it, as the private-only guard already treated that host. Upgrade note: a routes.json rule or alias written with the ssh.github.com/… host no longer matches; rewrite it as github.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 install keeps the existing unit's ExecStart arguments and replaces only the --config/--routes given on that run (binary path refreshed), so a bare install (as the doctor/status warning suggests) can no longer drop them; a changed ExecStart prints the old and new lines and says to restart; ExecStart is matched only in [Service] and CRLF units compare equal; an unreadable unit warns "cannot read the service unit"; a symlinked unit is refused before systemctl --user disable runs. Upstream candidate.
  • Autostart fix: the Linux systemd user unit is WantedBy=default.target (kardianos/service wrote multi-user.target, which a systemctl --user manager never reaches, so the daemon did not start after a reboot although the unit showed "enabled"); install (re-run) and start repair an existing unit and re-enable it (a unit counts as fine only with WantedBy=default.target and its default.target.wants link; an unreachable user manager changes nothing and prints the manual steps), a re-run install with new --config/--routes rewrites ExecStart, status and doctor warn about an unconfirmed one, and the macOS launchd agent sets RunAtLoad (kardianos/service defaults it to false, so in v0.3.0-ev.1 the 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 --all and list --all likewise (#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 files rekey-pq keeps) 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-cmd and 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-cmd must differ for different keys, or it exits 2, and a final read-back reports an item a later store overwrote; it ends with an N stored (M verified, K UNVERIFIED, V verify-failed), F store-failed line, and turns core dumps off. Also claude-memsync version --json (and claude-memmerge version --json): one JSON line with name, version, commit, go, os, arch; the plain version text line is unchanged, any other argument exits 2, and both are documented as stable. Upstream candidate.
  • Faster, selectable host bootstrap (#4): scripts/bootstrap.sh takes --project/--skip-project to choose which checkouts get a mem-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-classic keeps 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

Pre-release

Choose a tag to compare

@gitgat gitgat released this 26 Oct 10:40

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-pq converts an existing single-member classic repo, optionally onto one fresh root commit. It shows a dry run first, resumes when rerun, can --rollback a migration that was not pushed, and retire-classic retires the old key.
  • One config directory. routes.json, apps.json and the age identity all live in ${XDG_CONFIG_HOME:-~/.config}/claude-memsync on 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.sh sets 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-dir installs the binaries from a release archive instead of building from source.
  • macOS. Stock /bin/bash 3.2 and BSD tools are enough. A standalone binary in ~/.local/bin is 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.
    • --repo also accepts the GitHub repo name.
    • join status shows 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/bin links at the new build, and start the daemon again. The filters repoint themselves.
  • Re-run claude-memsync doctor --install-timer so 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.