This is an automated email from the ASF dual-hosted git repository.
potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow-steward.git
The following commit(s) were added to refs/heads/main by this push:
new f20efcd docs(setup): document sync.sh extensions for memory and PATH
(#300)
f20efcd is described below
commit f20efcd66686f8c51732ae842b80c7aba4adada5
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue May 26 18:39:51 2026 +0200
docs(setup): document sync.sh extensions for memory and PATH (#300)
The minimal sync.sh template doesn't cover two patterns that show
up in practice on multi-host setups: sharing per-project memory and
exposing tracked scripts on $PATH. Both want $HOME-relative keys so
the same repo entry resolves on /home/you and /Users/you.
Adds two follow-on subsections to "Syncing user-scope config across
machines" with idempotent helper functions, and updates the
"What to track" table to flag ~/.claude/projects/<key>/memory/ as
optionally sharable.
Generated-by: Claude Code (Opus 4.7)
---
docs/setup/secure-agent-setup.md | 141 ++++++++++++++++++++++++++++++++++++++-
1 file changed, 140 insertions(+), 1 deletion(-)
diff --git a/docs/setup/secure-agent-setup.md b/docs/setup/secure-agent-setup.md
index 54e5806..9e89dd4 100644
--- a/docs/setup/secure-agent-setup.md
+++ b/docs/setup/secure-agent-setup.md
@@ -37,6 +37,8 @@
- [Layout](#layout)
- [Setting up a fresh host](#setting-up-a-fresh-host)
- [A minimal `sync.sh`](#a-minimal-syncsh)
+ - [Extending `sync.sh`: share project memory across
machines](#extending-syncsh-share-project-memory-across-machines)
+ - [Extending `sync.sh`: expose tracked scripts on
`$PATH`](#extending-syncsh-expose-tracked-scripts-on-path)
- [Why a *private* repo](#why-a-private-repo)
- [Adopter setup](#adopter-setup)
- [Direct manual install](#direct-manual-install)
@@ -1176,7 +1178,7 @@ paths). Track the artifacts you want shared, symlink them
into
|---|---|
| `CLAUDE.md` (personal collaboration prefs) | `~/.claude/.credentials.json` —
⚠ secret, never commit |
| `scripts/sandbox-bypass-warn.sh`, `scripts/sandbox-error-hint.sh`,
`scripts/sandbox-status-line.sh`, and any other hooks | `~/.claude/sessions/`,
`~/.claude/history.jsonl` — session state |
-| `agent-isolation/claude-iso.sh` (if you globally installed it per the
wrapper section) | `~/.claude/projects/` — per-project memory and tasks |
+| `agent-isolation/claude-iso.sh` (if you globally installed it per the
wrapper section) | `~/.claude/projects/<key>/` — per-project session state and
tasks (the `memory/` subdir is optionally sharable, see [Extending `sync.sh`:
share project memory across
machines](#extending-syncsh-share-project-memory-across-machines)) |
| Custom slash commands (`commands/<name>.md`) | `~/.claude/settings.json` —
typically differs per host (plugins, statusLine paths, voice) |
| MCP servers you've audited and want everywhere (`.mcp.json` shape, by hand)
| `~/.claude/settings.local.json` — by design machine-specific |
@@ -1261,6 +1263,143 @@ git diff --cached --quiet || \
git log @{u}.. --oneline | grep -q . && git push
```
+### Extending `sync.sh`: share project memory across machines
+
+Claude Code persists durable per-project memory under
+`~/.claude/projects/<key>/memory/`, where `<key>` is the project's
+absolute working directory with `/` and `.` replaced by `-`. The same
+project takes a different key on each host
+(`-home-you-code-foo` on Linux vs `-Users-you-code-foo` on macOS), so
+a naive copy-the-tree-into-the-repo sync either misses the cross-host
+mapping or stomps over it.
+
+The pattern that works: store memories in the repo under a
+`$HOME`-relative subdir, and have `sync.sh` re-establish a per-host
+symlink after every pull. The function below is idempotent — it
+ingests any non-symlink memory dir found on the host that is not yet
+in the repo, then re-points the runtime symlinks at the repo paths.
+New project on a new host? Open it once; the next sync pass picks up
+the memory dir, ingests it, and the symlink appears on every other
+host on their next pull.
+
+```bash
+MEM_REPO="$HOME/.claude-config/memory"
+PROJECTS="$HOME/.claude/projects"
+
+# Encode an absolute path the way Claude Code keys project dirs: every
+# / and . becomes -. So /home/you/.claude-config -> -home-you--claude-config.
+encode_path() {
+ local p="$1"
+ p="${p//\//-}"
+ p="${p//./-}"
+ printf '%s' "$p"
+}
+
+ensure_memory_links() {
+ mkdir -p "$MEM_REPO"
+ local home_key
+ home_key="$(encode_path "$HOME")"
+
+ # Step 1 — ingest any non-symlink memory dir not yet in the repo.
+ for project_dir in "$PROJECTS"/*/; do
+ runtime_mem="${project_dir}memory"
+ [[ -d "$runtime_mem" && ! -L "$runtime_mem" ]] || continue
+ [[ -n "$(ls -A "$runtime_mem" 2>/dev/null)" ]] || continue
+
+ key="$(basename "${project_dir%/}")"
+ if [[ "$key" == "$home_key" ]]; then
+ norm="_root_"
+ elif [[ "$key" == "$home_key-"* ]]; then
+ norm="${key#$home_key-}"
+ else
+ # Project lives outside $HOME — preserve full key under ABS-.
+ norm="ABS$key"
+ fi
+
+ repo_mem="$MEM_REPO/$norm"
+ [[ -e "$repo_mem" ]] && continue
+ mv "$runtime_mem" "$repo_mem"
+ done
+
+ # Step 2 — re-establish per-host symlinks for every tracked memory dir.
+ for repo_mem in "$MEM_REPO"/*/; do
+ [[ -d "$repo_mem" ]] || continue
+ norm="$(basename "${repo_mem%/}")"
+ if [[ "$norm" == "_root_" ]]; then
+ key="$home_key"
+ elif [[ "$norm" == ABS-* ]]; then
+ key="${norm#ABS}"
+ else
+ key="$home_key-$norm"
+ fi
+ target="$PROJECTS/$key/memory"
+ mkdir -p "$(dirname "$target")"
+ if [[ -L "$target" ]]; then
+ [[ "$(readlink "$target")" == "${repo_mem%/}" ]] && continue
+ rm "$target"
+ elif [[ -d "$target" ]]; then
+ continue # real dir not yet ingested — leave alone
+ fi
+ ln -s "${repo_mem%/}" "$target"
+ done
+}
+```
+
+Call `ensure_memory_links` from `sync.sh` *after* `git pull` (untracked
+files are not autostashed, so ingesting before pull risks colliding with
+a remote add of the same path).
+
+### Extending `sync.sh`: expose tracked scripts on `$PATH`
+
+A second helper, dropped into the same `sync.sh`, symlinks every
+tracked executable into `~/.local/bin/` so the scripts are invocable
+by name from any shell. Platform-suffixed binaries (`foo-linux`,
+`foo-macos`) link as the bare `foo` on the matching host only — so the
+same repo can carry both builds and each host picks up the right one.
+
+```bash
+LOCAL_BIN="$HOME/.local/bin"
+REPO="$HOME/.claude-config"
+
+ensure_bin_links() {
+ mkdir -p "$LOCAL_BIN"
+ local platform=""
+ case "$(uname -s)" in
+ Linux) platform=linux ;;
+ Darwin) platform=macos ;;
+ esac
+
+ link_one() {
+ local src="$1" name="$2" dst="$LOCAL_BIN/$2"
+ if [[ -L "$dst" ]]; then
+ [[ "$(readlink "$dst")" == "$src" ]] && return
+ rm "$dst"
+ elif [[ -e "$dst" ]]; then
+ return # something non-symlink is in the way — leave alone
+ fi
+ ln -s "$src" "$dst"
+ }
+
+ for f in "$REPO"/bin/* "$REPO"/scripts/*.sh; do
+ [[ -f "$f" && -x "$f" ]] || continue
+ name="$(basename "$f")"
+ case "$name" in
+ *-linux) [[ "$platform" == "linux" ]] && link_one "$f" "${name%-linux}"
;;
+ *-macos) [[ "$platform" == "macos" ]] && link_one "$f" "${name%-macos}"
;;
+ *) link_one "$f" "$name" ;;
+ esac
+ done
+}
+```
+
+With this in place, no one-shot symlink step is needed when wiring a
+fresh host for scripts in `bin/` or `scripts/` — the next sync pass
+takes care of it. The hooks referenced by absolute path from
+`settings.json` (e.g. `~/.claude/scripts/sandbox-bypass-warn.sh`) still
+need their one-time symlink as in
+[Setting up a fresh host](#setting-up-a-fresh-host) — these run from
+the harness, not the user shell.
+
### Why a *private* repo
Three reasons make this non-negotiable: