Sync the local Obsidian vault with a Cloudflare R2 bucket using rclone (S3 + crypt). Triggered by `/sync-notes`. Defaults to bidirectional sync; understands...
---
name: sync-notes
description: Sync the local Obsidian vault with a Cloudflare R2 bucket using rclone (S3 + crypt). Triggered by `/sync-notes`. Defaults to bidirectional sync; understands natural-language flags for partial download/upload by glob.
version: 1.0.1
metadata:
{"openclaw":{"emoji":"☁️","requires":{"bins":["rclone","bash"],"env":["CLOUD_NOTES_PATH"]}}}
---
# sync-notes Skill
Sync the local notes directory (`$CLOUD_NOTES_PATH`) with a **Cloudflare R2** bucket through an
end-to-end encrypted **rclone crypt** remote. The skill is a thin orchestrator over a single bash
entry point at `scripts/sync.sh`; the LLM's job is to translate the user's natural-language intent
into the right subcommand.
## Trigger
Activate **only** when the user message starts with `/sync-notes` (with or without trailing
arguments). Examples:
| User message | Intent | Action |
|---|---|---|
| `/sync-notes` | full bidirectional sync | `sync.sh bisync` |
| `/sync-notes setup` | run the configuration wizard | `sync.sh setup` |
| `/sync-notes init` | establish the bisync baseline (first run only) | `sync.sh init` |
| `/sync-notes status` | show config + last-sync info | `sync.sh status` |
| `/sync-notes 下载 welcome` | download files matching `welcome` | `sync.sh download '**welcome**'` |
| `/sync-notes upload daily/*.md` | upload glob | `sync.sh upload 'daily/*.md'` |
| `/sync-notes 把 ideas 目录拉下来` | download a directory | `sync.sh download 'ideas/**'` |
## How to interpret natural language
After stripping the `/sync-notes` prefix, classify the remainder:
1. **Empty** → run `bisync` (default bidirectional sync).
2. Contains a recognised keyword:
- `setup`, `配置`, `wizard` → `setup`
- `init`, `初始化`, `baseline` → `init`
- `status`, `状态`, `信息` → `status`
3. Mentions **download** intent (`下载`, `拉`, `pull`, `download`, `get`, `取回`):
- Extract the target token(s) and convert to a glob (see "Glob rules" below).
- Run `sync.sh download '<glob>'`.
4. Mentions **upload** intent (`上传`, `推`, `push`, `upload`, `send`):
- Same as above with `sync.sh upload '<glob>'`.
5. Otherwise: ask the user to clarify; do not guess.
### Glob rules
Globs are passed verbatim to rclone as `--include` filters and apply to *plain* (decrypted) paths.
- A bare keyword such as `welcome` becomes `**welcome**` (matches anywhere in path).
- A directory name like `ideas` becomes `ideas/**`.
- Paths already containing wildcards (`*`, `?`, `[`) are passed through unchanged.
- Filenames without extension default to matching `<name>` and `<name>.md`; combine with `**` if
unsure. When in doubt, run `sync.sh download '<glob>' --dry-run` first.
## Workflow contract
1. **First run ever:** the script refuses `bisync` until a baseline exists. Tell the user to run
`/sync-notes init` after confirming both sides are in the desired starting state.
2. **Backups:** before any write-side operation (`bisync`, `download`, `init`), the local notes
directory is mirrored to `backups/latest/` (single rolling copy, previous one is replaced).
3. **Conflicts:** bisync runs with conflict markers. After every run the script scans for
`*.conflict*` files; if any are found the script exits non-zero and lists them. **Surface this
list to the user verbatim and ask which side to keep**—do not auto-resolve.
4. **Logs:** every invocation appends to `logs/sync.log` and emits a per-run file
`logs/<UTC-timestamp>.log` for the rclone output.
## Filesystem layout
```
skills/sync-notes/
├── SKILL.md # this file
├── README.md # short user-facing notes (optional)
├── config/
│ ├── rclone.conf # active rclone config (created by setup, gitignored)
│ ├── rclone.conf.example # template
│ ├── .env # non-rclone settings (created by setup, gitignored)
│ └── .env.example # template
├── scripts/
│ ├── sync.sh # main entry point (dispatcher)
│ └── setup.sh # interactive configuration wizard
├── state/
│ └── bisync/ # rclone bisync workdir (listings + lockfiles)
├── logs/ # rolling logs
├── backups/
│ └── latest/ # most recent local snapshot (single copy)
└── .gitignore
```
`scripts/sync.sh` derives every path from its own location (`$(dirname "$0")/..`); no absolute
paths are hard-coded.
## Required environment
| Variable | Purpose | Required |
|---|---|---|
| `CLOUD_NOTES_PATH` | absolute path of the local notes directory | ✅ |
The script aborts early with a clear message if `CLOUD_NOTES_PATH` is unset, missing, or not a
directory.
## Configuration files
### `config/rclone.conf`
Native rclone format with **two remotes**:
```ini
[r2-raw]
type = s3
provider = Cloudflare
access_key_id = <ACCESS_KEY_ID>
secret_access_key = <SECRET_ACCESS_KEY>
endpoint = https://<ACCOUNT_ID>.r2.cloudflarestorage.com
acl = private
[notes]
type = crypt
remote = r2-raw:<BUCKET>/<OPTIONAL_PREFIX>
filename_encryption = standard
directory_name_encryption = true
password = <OBSCURED>
password2 = <OBSCURED>
```
### `config/.env`
Non-rclone settings consumed by `sync.sh`:
```sh
# Name of the crypt remote in rclone.conf
RCLONE_REMOTE=notes
# Number of local backup snapshots to retain
BACKUP_KEEP=1
# Extra rclone flags (optional)
RCLONE_EXTRA_FLAGS="--transfers=4 --checkers=8"
```
`.example` versions ship with the skill; the wizard fills in the real values.
## Subcommands implemented by `sync.sh`
| Subcommand | Behaviour |
|---|---|
| `setup` | Run `setup.sh`; collects R2 creds + crypt password(s); writes `rclone.conf` and `.env` with `chmod 600`. Idempotent: confirms before overwriting. |
| `init` | First-time baseline: `rclone bisync --resync`. Refuses to run if a baseline already exists unless `--force` is passed. |
| `bisync` (default) | `rclone bisync $LOCAL notes:` with conflict reporting. Backs up first. |
| `download <glob>` | `rclone copy notes: $LOCAL --include '<glob>'`. Backs up first. Defaults to `--dry-run` only when the user said "试" / "dry"; otherwise runs for real. |
| `upload <glob>` | `rclone copy $LOCAL notes: --include '<glob>'`. No local backup needed; remote is authoritative. |
| `status` | Prints config validity, last-sync timestamp, conflict files (if any), backup state. |
## Safety rules for the LLM
- **Never** write secrets into chat. If the user pastes secrets, immediately move them into
`config/rclone.conf` / `config/.env` and remove from any visible context.
- **Never** invoke `rclone` with `--config` pointing somewhere outside this skill.
- **Never** run `bisync` if `status` reports unresolved conflicts; ask first.
- When `download`/`upload` glob match count is ambiguous, run with `--dry-run` first and show the
list of matches.
- Backups are single-copy; if you are about to do something risky (large delete, schema change),
copy `backups/latest/` to `backups/manual-<UTC>/` manually before proceeding and tell the user.
## First-time onboarding (what to tell the user)
```
1. /sync-notes setup # fill in R2 + crypt creds
2. export CLOUD_NOTES_PATH=... # point at the local notes dir
3. /sync-notes init # establish the bisync baseline
4. /sync-notes # routine bidirectional sync
```
don't have the plugin yet? install it then click "run inline in claude" again.
added explicit decision tree for natural-language parsing, edge cases for rate limits and auth errors, blob rules clarified, secret handling safety rules moved to decision points, network timeout scenarios, and explicit edge cases on glob matching and conflict resolution.
sync-notes orchestrates bidirectional sync between a local Obsidian vault and a Cloudflare R2 bucket via rclone's crypt remote, encrypting data end-to-end. triggered by /sync-notes prefix, it translates natural-language intent (setup, init, status, download, upload, or default bisync) into bash subcommands. use this when the user needs to push local notes to the cloud, pull remote notes locally, establish initial baseline, configure credentials, or check sync status.
Environment variables (required):
CLOUD_NOTES_PATH: absolute path to the local notes directory. skill aborts with clear error if unset, missing, or not a directory.External connections:
ACCESS_KEY_ID, SECRET_ACCESS_KEY, ACCOUNT_ID, and bucket name. stored in config/rclone.conf after setup wizard.$PATH. skill checks for presence.scripts/sync.sh and scripts/setup.sh.Configuration files (created and maintained by setup wizard):
config/rclone.conf: native rclone format with two remotes (r2-raw for raw S3, notes for crypt overlay). gitignored. created with chmod 600. template at config/rclone.conf.example.config/.env: non-rclone settings (RCLONE_REMOTE, BACKUP_KEEP, RCLONE_EXTRA_FLAGS). gitignored. created with chmod 600. template at config/.env.example.State and logs:
state/bisync/: rclone bisync workdir (listings, lock files). created by rclone on first init.logs/sync.log: rolling append-only log of all invocations.logs/<UTC-timestamp>.log: per-run rclone output (created each invocation).Backups:
backups/latest/: single rolling mirror of local notes directory created before any write operation. previous backup is replaced (not retained). user can manually copy to backups/manual-<UTC>/ before risky operations.Receive user message starting with /sync-notes.
/sync-notes download ideas).sync.sh.Parse natural-language intent from the suffix (everything after /sync-notes).
setup, init, bisync, download, upload, status) and glob pattern (if applicable).For download and upload subcommands, convert natural-language target into glob pattern.
**keyword**; directory becomes dirname/**; paths with wildcards pass through unchanged; filenames without extension match <name> and <name>.md.**ideas** or ideas/** or **welcome**).Validate environment before execution.
CLOUD_NOTES_PATH environment variable.CLOUD_NOTES_PATH is set, exists, and is a readable directory.$PATH.Execute the appropriate sync.sh subcommand.
--dry-run flag.scripts/sync.sh <subcommand> [<glob>] [--dry-run] from the skill root (derived from script location).config/rclone.conf and crypt remote named in config/.env.logs/.Capture and parse rclone output.
bisync, download, upload: scan for conflict files matching *.conflict*.Report results to user.
After parsing the suffix, classify intent:
bisync (default bidirectional sync).setup, 配置, wizard: run setup (configuration wizard). user must complete this before first sync.init, 初始化, baseline: run init (establish bisync baseline). refuse if baseline already exists unless user passes --force.status, 状态, 信息: run status (print config, last-sync time, conflicts, backup state).下载, 拉, pull, download, get, 取回):--dry-run only (show matches, do not transfer).--dry-run result first.上传, 推, push, upload, send):sync.sh upload '<glob>' immediately (no --dry-run unless user explicitly asked).Before bisync, download, or init (any write-side op):
status first to check for unresolved conflicts.On network or auth errors:
rclone.conf are wrong. ask user to re-run /sync-notes setup.rclone.conf is correct and Cloudflare infrastructure is reachable.On glob match edge cases:
On conflict handling:
*.conflict* files after each run.On secret handling:
config/rclone.conf or config/.env, remove from visible context, and remind user that secrets are stored locally with chmod 600.On success (all subcommands):
logs/<UTC-timestamp>.log containing rclone output.logs/sync.log with timestamp, subcommand, and summary (files transferred, duration, status).bisync or download: local snapshot created at backups/latest/ (replaces previous).bisync or download with conflicts: exit code becomes non-zero; conflict file list written to stdout/stderr and logged.--progress=json).On conflict (bisync, download, or init with existing baseline):
*.conflict* files output to stdout (one per line, with full path).On configuration error (missing rclone.conf, invalid .env, unset CLOUD_NOTES_PATH):
/sync-notes setup to create config" or "set CLOUD_NOTES_PATH=/path/to/notes".On network or rclone error:
logs/<UTC-timestamp>.log.Status subcommand output:
logs/sync.log.*.conflict* files if any; else "none".backups/latest/ and timestamp of last write.bisync: user receives confirmation message showing count of files synced, direction (push/pull/both), and timestamp. if conflicts, conflict list appears.download or upload: user sees count of files transferred and size in bytes. if --dry-run, user sees count of files that would be transferred without actually moving data.init: user sees "baseline established" or "bisync lockfile created at state/bisync/.../".setup: user sees "config written to config/rclone.conf and config/.env with secure permissions (mode 600)".status: user sees formatted output with current config state, last sync time, and any pending conflicts.logs/<UTC-timestamp>.log and logs/sync.log has been appended.backups/latest/ (viewable with ls -la) after any write operation.