Command-line client for the Grug Notes /api/v1/ API.
The /api/v1/ API is public API surface. New server endpoints may be callable
by custom clients before this CLI exposes first-class commands for them.
pip install grugnotes-cli
grugnotes auth # opens settings page, paste your API key
grugnotes status # verify connectionAPI keys are fixed to one space. A key may be broad within that space or limited to a selected prompt allowlist. Permissions are set at creation; create a new key if you need different access.
grugnotes authstores the API key in plain text at~/.grugnotes. The CLI sets that file to owner-only permissions (0600) and refuses symlinked config paths.sync init --save-keyandsync reset --save-keysave the active API key into.grugnotes.jsonso each sync directory remembers its own key. This is useful when you have multiple API keys for different spaces. Keys from--api-keyorGRUGNOTES_API_KEYare ephemeral by default and require--save-keyto persist. Keys from~/.grugnotes(global config) are always saved automatically. Precedence:--api-key>GRUGNOTES_API_KEY>.grugnotes.json(local) >~/.grugnotes(global).- For CI, shared machines, or ephemeral shells, prefer
GRUGNOTES_API_KEYinstead of saving a key locally. - The CLI refuses plain
http://base URLs unless the host is loopback (localhost,127.0.0.1, or::1). To deliberately use insecure HTTP against a non-localhost host, pass--allow-insecure-httpor setGRUGNOTES_ALLOW_INSECURE_HTTP=1. - Avoid
--api-keywhen possible; shell history and process lists can leak it. Prefergrugnotes authorGRUGNOTES_API_KEY. - Saved API keys are bound to the base URL they were stored with. If you switch
--base-urlorGRUGNOTES_BASE_URL, the CLI refuses to reuse the saved key unless you provide an explicit key for that host. - Sync commands refuse to operate on symlinked sync directories or directories that contain symlinked sync paths or prompt metadata directories.
sync initandsync resetwrite a local.gitignorecovering.grugnotes/,.grugnotes.json,.prompt.json, and*.conflict.md.
On Apple Silicon, run CLI tests under the x86_64 virtualenv so the xxhash wheel
matches the interpreter architecture:
source activate.sh && arch -x86_64 python -m pytest cli/grugnotes_cli/tests -qLegacy .grugnotes.json entries that used sha256:... synced hashes are migrated
in place to xxhash as files are scanned (sync status, sync push, sync pull,
or sync watch startup). This avoids a one-time "push everything" after upgrade.
Legacy state entries with deleted_remotely=true are also migrated. During migration:
- unchanged local tracked files are removed;
- locally edited tracked files are moved to
.grugnotes/trash/....
grugnotes notes # all notes
grugnotes notes daily-notes # filter by prompt
grugnotes notes --from 2026-03-01 --to 2026-03-03
grugnotes notes --page 2 --limit 50 # paginationgrugnotes read 42 # by id
grugnotes read 2026-03-03 # list notes from a date
grugnotes read daily-notes # list notes from a prompt
grugnotes read daily-notes/2026-03-03 # by prompt + filename slug
grugnotes read daily-notes/2026-03-03-2 # suffixed same-date siblinggrugnotes create daily-notes "Hello world"
grugnotes create daily-notes "More text" --date 2026-03-03
grugnotes create daily-notes "Append this" --appendgrugnotes edit 42 --notes "Full replacement"
grugnotes edit 42 --old "typo" --new "fixed"grugnotes prompts # list all
grugnotes prompts --page 2 --limit 50 # pagination
grugnotes prompt daily-notes # show one promptSearches synced prompt metadata locally (no API call). Matches by exact name, then startswith, then contains.
grugnotes prompts-search dailygrugnotes sync init
grugnotes sync init --prompt daily-notes
grugnotes sync init --from 2026-03-01 --to 2026-03-03
grugnotes sync init --save-key # persist API key in this directory
grugnotes sync status
grugnotes sync pull
grugnotes sync push
grugnotes sync reset
grugnotes sync reset --save-key # persist API key on reset
grugnotes sync watch # continuous sync
grugnotes sync watch --interval 60 # poll every 60s (default 30)All sync subcommands except watch support --dry-run to preview changes
without writing files. Sync commands exit with code 2 on unresolved conflicts.
sync watch polls at the configured interval while there is recent sync activity,
then slows remote checks to 60s after 5 minutes of quiet time. If the remote hash
signal is temporarily unavailable, watch mode tolerates 3 unavailable poll cycles
before falling back to a direct pull.
Poll sleeps add up to 25% jitter to avoid synchronized bursts across many clients.
The CLI tracks three versions of each note: the remote copy on the server,
the local .md file, and a shadow (the last content both sides agreed on,
stored in .grugnotes/shadows/).
On pull, the CLI fetches notes changed since the last sync. If only the remote
changed, the local file is updated. If both sides changed the same note, a
.conflict.md file is written and further syncs are blocked until you resolve it
by deleting the conflict file.
On push, the CLI diffs local files against their shadows and sends a 3-way merge patch to the server. The server rejects the push if someone else saved in between (you'll need to pull first).
watch runs both continuously: a file watcher pushes local edits (debounced 3s), while a lightweight poll checks for remote changes every 30–60s.
File names mirror the server's canonical filename_slug. Untitled same-date
siblings use suffixed files such as 2026-03-03-2.md; creating such a file and
pushing it creates another untitled block on 2026-03-03.
After upgrading from older CLI versions, the first pull may rename files to match
server canonical filename slugs. Renames are keyed by remote block ID; if a target
path is occupied, sync writes a .conflict.md file instead of overwriting it.
When a script modifies synced files (e.g. appending changelog entries), always pull before editing to avoid overwriting changes made on the server:
grugnotes sync pull "$DIR" # get latest from server
echo "new content" >> "$DIR/file.md" # edit local files
grugnotes sync push "$DIR" # push changes backPulls are incremental — only notes modified since the last sync are fetched.
Most commands accept --json to output machine-readable data instead of
human-formatted text. sync watch is the main exception because it is a
long-running log stream. Useful for scripting:
grugnotes read daily-notes --json | jq '.data[].date'
grugnotes notes --json | jq '.data | length'grugnotes --base-url https://grugnotes.com statusOr use env vars:
export GRUGNOTES_API_KEY="gn_..."
export GRUGNOTES_BASE_URL="https://grugnotes.com"The package includes an AGENTS.md skill file with structured instructions
for AI coding agents. After install, find it with:
python -c "import importlib.resources; print(importlib.resources.files('grugnotes_cli').parent / 'AGENTS.md')"