From 34666b764dea00224800d014540b2f40e9d88fc2 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Mon, 26 Oct 2026 12:24:52 -0700 Subject: [PATCH] fix(handoff-sync): move the local handoff folder to ~/.local/state The Claude Code sandbox denies writes under ~/.claude, so on a host with no Drive mount, prepare-handoff cannot create ~/.claude/handoffs. The default local folder is now ~/.local/state/claude-handoffs, which the sandbox allows. HANDOFF_LOCAL_DIR still overrides it. A new test pins the default. It fails against main's copy. TILSIT needs a one-time migration after this merges, because rclone bisync keys its state on the local/remote path pair: 1. git -C ~/Developer/scripts pull 2. mv ~/.claude/handoffs ~/.local/state/claude-handoffs 3. rclone bisync ~/.local/state/claude-handoffs 'gdrive:Claude Handoff' --resync --drive-skip-gdocs 4. Check the next cron run in ~/.local/state/rclone/handoff-sync.log The handoff skills in claude-config move to the same folder in a second PR, after this migration, so TILSIT never writes a handoff that cron does not sync. Advances smartwatermelon/claude-config#779 (item 3). --- README.md | 2 +- handoff-sync.sh | 7 +++++-- tests/handoff-sync.bats | 5 +++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bcdf12f..91aa04d 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Internal repo; not intended for public consumption. | `1password-mcp-launcher.sh` | Reads `OP_SERVICE_ACCOUNT_TOKEN` from the macOS Keychain and launches the 1Password MCP server at a pinned version — avoids handing the keychain-held token to whatever `npx -y` happens to resolve. | | `instapaper-mcp-launcher.sh` | Reads the four Instapaper API credentials from the macOS Keychain and launches the Instapaper MCP server. The server calls bare `dotenv.config()`, which resolves `.env` against the process working directory — a Desktop-spawned server never runs from the repo, so exporting here is what actually reaches it. | | `claude-config-backup.sh` | Snapshots machine-local Claude config files into a git repo and pushes. Gates each file on quiescence (unchanged size+mtime across a settle window), valid JSON, and a `gitleaks`-first secret scan before committing. Driven by `com.asiago.claude-config-backup` every 15 min. | -| `handoff-sync.sh` | Two-way `rclone bisync` of `~/.claude/handoffs` with the Google Drive folder `Claude Handoff`, for hosts with no Drive app (TILSIT). Runs from `andrewrich`'s crontab every 5 minutes, not a LaunchAgent, because that user has no GUI session there. Log: `~/.local/state/rclone/handoff-sync.log`. The `gdrive:` remote uses rclone's shared client ID, which Google may retire. Tests in `tests/handoff-sync.bats`. | +| `handoff-sync.sh` | Two-way `rclone bisync` of `~/.local/state/claude-handoffs` with the Google Drive folder `Claude Handoff`, for hosts with no Drive app (TILSIT). Runs from `andrewrich`'s crontab every 5 minutes, not a LaunchAgent, because that user has no GUI session there. Log: `~/.local/state/rclone/handoff-sync.log`. The `gdrive:` remote uses rclone's shared client ID, which Google may retire. Tests in `tests/handoff-sync.bats`. | | `refresh-claude-secrets.sh` | Resolves `op://` references from `secrets.op` and writes plaintext values into `claude-secrets.md` for Claude via the Filesystem MCP. Re-run after rotating any referenced credential. Must run from `~/.config/claude-code` (the symlink target). | | `ssh` | Wrapper over the real `ssh` that intercepts `# op: VAR="op://..."` annotations in `~/.ssh/config`, resolves them via 1Password, exports, then `exec`s real `ssh`. Unannotated hosts pass straight through. Spec: [`docs/plans/2026-04-16-ssh-wrapper-spec.md`](docs/plans/2026-04-16-ssh-wrapper-spec.md). Tests in `tests/ssh-wrapper.bats`. | diff --git a/handoff-sync.sh b/handoff-sync.sh index 3368204..e68de8f 100755 --- a/handoff-sync.sh +++ b/handoff-sync.sh @@ -1,14 +1,17 @@ #!/usr/bin/env bash set -euo pipefail -# Two-way sync of ~/.claude/handoffs with Drive "Claude Handoff". TILSIT cron, every 5 min: no GUI session there for a LaunchAgent. +# Two-way sync of ~/.local/state/claude-handoffs with Drive "Claude Handoff". TILSIT cron, every 5 min: no GUI there for a LaunchAgent. # Auth errors in the log: rclone's shared client_id is likely retired. Fix with: rclone config reconnect gdrive: # Full path, not command -v: cron's PATH lacks /opt/homebrew/bin. Tests stub it; empty or non-executable = missing. RCLONE_BIN="${RCLONE_BIN:-/opt/homebrew/bin/rclone}" -HANDOFF_LOCAL_DIR="${HANDOFF_LOCAL_DIR:-${HOME}/.claude/handoffs}" +# Not under ~/.claude: the Claude Code sandbox denies writes there, so a skill could not save a handoff (claude-config#779). + +# After moving this dir, run bisync once by hand with --resync: rclone keys its sync state on the path pair. +HANDOFF_LOCAL_DIR="${HANDOFF_LOCAL_DIR:-${HOME}/.local/state/claude-handoffs}" HANDOFF_REMOTE="${HANDOFF_REMOTE:-gdrive:Claude Handoff}" HANDOFF_LOG_DIR="${HANDOFF_LOG_DIR:-${HOME}/.local/state/rclone}" diff --git a/tests/handoff-sync.bats b/tests/handoff-sync.bats index 63e8cbe..16b2332 100755 --- a/tests/handoff-sync.bats +++ b/tests/handoff-sync.bats @@ -122,6 +122,11 @@ EOF [ "${output}" = "/opt/homebrew/bin/rclone" ] } +@test "the default local folder is under ~/.local/state, which the sandbox allows" { + run env -u HANDOFF_LOCAL_DIR HOME="${TMPDIR_T}/home" bash -c "source '${REPO_ROOT}/handoff-sync.sh'; echo \"\${HANDOFF_LOCAL_DIR}\"" + [ "${output}" = "${TMPDIR_T}/home/.local/state/claude-handoffs" ] +} + @test "sourcing the script does not run main" { [ "$(type -t main)" = "function" ] [ ! -e "${RCLONE_ARGS}" ]