235
Two-way synchronization between Google Keep notes and local Markdown files.
Notes labeled sync in Google Keep are synchronized to/from local markdown files, and new local markdown files are pushed to Google Keep (and created with a sync label).
sync label) first, then delete the local Markdown file - a file deleted while its note is still labeled in Keep is simply recreated on the next run.gkeepapi client.python -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
Edit config.py or set environment variables (env vars take precedence):
| key | env var | meaning |
|---|---|---|
gmail_user | GKEEP_GMAIL_USER | your Gmail address |
sync_label | GKEEP_SYNC_LABEL | Keep label that marks notes for syncing (default sync) |
log_file_path | GKEEP_LOG_FILE_PATH | path for log file in addition to stdout (optional) |
master_token_file_path | GKEEP_MASTER_TOKEN_FILE_PATH | path to a secret file containing the master token (optional) |
| (no config key) | GOOGLE_MASTER_TOKEN | master token passed directly via env var (takes precedence over master_token_file_path; see Authentication) |
markdown_notes_dir | GKEEP_MARKDOWN_DIR | local directory for the markdown files |
db_path | GKEEP_DB_PATH | sqlite file holding change-detection state (default ~/.config/gkeep-md-sync/state.db) |
conflict_policy | GKEEP_CONFLICT_POLICY | fail, skip, use-gkeep, or use-md (details below) |
The application uses one's Google master token to authenticate to Google Keep.
The master token is looked up in the following order:
GOOGLE_MASTER_TOKEN.master_token_file_path (in config.py).If no master token is found in the first two locations and the application is run interactively, it will prompt for a Google OAuth token (which the application exchanges for the master token).
To obtain the OAuth token:
oauth_token browser cookie.The OAuth token is only used to obtain a master token. It is not stored by the application.
The acquired master token is saved automatically to master_token_file_path, so subsequent unattended runs authenticate on their own. Alternatively, export it as GOOGLE_MASTER_TOKEN.
The app saves an interactively acquired master token to the file configured by master_token_file_path; you can also create it manually:
mkdir -p ~/.config/gkeep-md-sync
chmod 700 ~/.config/gkeep-md-sync
echo 'aas_et/*********************************' > ~/.config/gkeep-md-sync/master.token
chmod 600 ~/.config/gkeep-md-sync/master.token
.venv/bin/python main.py
Add a crontab -e expression:
* * * * * cd "$HOME/projects/gkeep-md-sync" && "$HOME/projects/gkeep-md-sync/.venv/bin/python" main.py
docker build -t maze88/gkeep-sync-md .
docker run --rm \
-e GOOGLE_MASTER_TOKEN=... \
-e [email protected] \
-e GKEEP_MARKDOWN_DIR=/app/notes/ \
-e GKEEP_DB_PATH=/app/state/state.db \
-v ~/Documents/notes/gkeep/:/app/notes/ \
-v ~/.config/gkeep-md-sync:/app/state \
maze88/gkeep-sync-md
The state DB must live on a persistent volume (GKEEP_DB_PATH + the second -v). With --rm and no DB mount it is recreated empty each run, so every run re-pulls all notes.
All config settings are overridable via env vars. Empty string disables the optional settings (GKEEP_LOG_FILE_PATH, GKEEP_MASTER_TOKEN_FILE_PATH).
Each note is a file with YAML frontmatter holding just its Keep identity, a # Title heading, and either freeform body text or a checklist:
---
google_keep_id: 17fca0582e0.05b9cd676d5cd6d5
---
# Groceries
- [ ] for sauce
- [x] tomatoes
- [ ] garlic
- [ ] basil
- [ ] pasta
- [x] olive oil
Checklist sub-items are written as two-space-indented task lines, mirroring Google Keep's single level of indentation.
google_keep_id links the file to its Keep note; a file without one is treated as new and created in Keep on the next run. Change-detection state (the content fingerprint) is not stored in the file — it lives in a sqlite database (db_path), so the app never rewrites a file except to pull real content from Keep. Before overwriting, the app re-hashes the file's saved content and withholds the pull if it changed since the last check, so a concurrent human save isn't clobbered. Opening or re-saving a file with identical content does not withhold the pull — only saved bytes that differ do.
A conflict is when a note changed in both Keep and its local file since the last sync. config.conflict_policy decides the outcome:
fail - abort the run.skip - leave both sides as-is.use-gkeep - Keep wins; overwrite the local file.use-md - local wins; overwrite the Keep note.Content type
Image
Digest
sha256:f59a65ac6…
Size
53.1 MB
Last updated
17 days ago
docker pull maze88/gkeep-sync-md