Install
openclaw skills install sync-notesSync 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.
openclaw skills install sync-notesSync 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.
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/**' |
After stripping the /sync-notes prefix, classify the remainder:
bisync (default bidirectional sync).setup, 配置, wizard → setupinit, 初始化, baseline → initstatus, 状态, 信息 → status下载, 拉, pull, download, get, 取回):
sync.sh download '<glob>'.上传, 推, push, upload, send):
sync.sh upload '<glob>'.Globs are passed verbatim to rclone as --include filters and apply to plain (decrypted) paths.
welcome becomes **welcome** (matches anywhere in path).ideas becomes ideas/**.*, ?, [) are passed through unchanged.<name> and <name>.md; combine with ** if
unsure. When in doubt, run sync.sh download '<glob>' --dry-run first.bisync until a baseline exists. Tell the user to run
/sync-notes init after confirming both sides are in the desired starting state.bisync, download, init), the local notes
directory is mirrored to backups/latest/ (single rolling copy, previous one is replaced).*.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.logs/sync.log and emits a per-run file
logs/<UTC-timestamp>.log for the rclone output.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.
| 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.
config/rclone.confNative rclone format with two remotes:
[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/.envNon-rclone settings consumed by sync.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.
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. |
config/rclone.conf / config/.env and remove from any visible context.rclone with --config pointing somewhere outside this skill.bisync if status reports unresolved conflicts; ask first.download/upload glob match count is ambiguous, run with --dry-run first and show the
list of matches.backups/latest/ to backups/manual-<UTC>/ manually before proceeding and 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