diff --git a/custom-skills/32-notion-writer/code/scripts/notion_writer.py b/custom-skills/32-notion-writer/code/scripts/notion_writer.py index ae63820..139102a 100644 --- a/custom-skills/32-notion-writer/code/scripts/notion_writer.py +++ b/custom-skills/32-notion-writer/code/scripts/notion_writer.py @@ -788,8 +788,12 @@ Examples: args = parser.parse_args() if not NOTION_TOKEN: - print("Error: NOTION_API_KEY not set in .env file") - print("Get your integration token from: https://www.notion.so/my-integrations") + print("Error: NOTION_API_KEY not set in environment.") + print("Preferred: fetch from 1Password at runtime —") + print(" NOTION_API_KEY=\"$(op read 'op://Development/Notion - Claude Agent/api-key')\" \\") + print(" python notion_writer.py …") + print("See SKILL.md → 'Credential handling' for full guidance.") + print("Token source: https://www.notion.so/my-integrations") sys.exit(1) notion = compat.make_client(NOTION_TOKEN) diff --git a/custom-skills/32-notion-writer/desktop/SKILL.md b/custom-skills/32-notion-writer/desktop/SKILL.md index 566f3d9..3c74555 100644 --- a/custom-skills/32-notion-writer/desktop/SKILL.md +++ b/custom-skills/32-notion-writer/desktop/SKILL.md @@ -12,8 +12,8 @@ Push markdown content to Notion pages or databases via Claude Code. ## Prerequisites - Python virtual environment at `~/Project/our-claude-skills/custom-skills/02-notion-writer/code/scripts/venv` -- Notion API key configured in `.env` file -- Target pages/databases must be shared with the integration +- Notion integration token (preferred: stored in 1Password — see [Credential handling](#credential-handling) below) +- Target pages/databases must be shared with the integration in Notion (Database/Page → ⋯ → Connections → add integration) ## Quick Start @@ -22,6 +22,70 @@ cd ~/Project/our-claude-skills/custom-skills/02-notion-writer/code/scripts source venv/bin/activate ``` +## Credential handling + +**Do NOT store the Notion API key in a `.env` file.** A long-lived plaintext secret on disk is unnecessary risk — Notion integration tokens grant write access to every page/database the integration is connected to. + +### Preferred: fetch from 1Password at runtime + +Store the token once in 1Password (Item: `Notion - Claude Agent`, Vault: `Development`, Field: `api-key`), then fetch on each invocation. The token lives only in process memory — never on disk, never in shell history. + +```bash +# One-shot push — token fetched + used + discarded in a single shell line +NOTION_API_KEY="$(op read 'op://Development/Notion - Claude Agent/api-key')" \ + python notion_writer.py --test +``` + +For repeated use in the same session, scope the variable to a subshell so it leaves no trace: + +```bash +( + export NOTION_API_KEY="$(op read 'op://Development/Notion - Claude Agent/api-key')" + python notion_writer.py --database "$DB_URL" --title "Foo" --file foo.md + python notion_writer.py --database "$DB_URL" --title "Bar" --file bar.md +) +# NOTION_API_KEY is gone from the parent shell +``` + +### Field name conventions in 1Password + +When you create the Notion integration item, use these field names: + +| Field | Value | +|---|---| +| `api-key` (CONCEALED) | The integration token (`ntn_...` for current API, `secret_...` for legacy) | +| `username` | Integration display name (e.g. "Claude Agent") | +| `hostname` | `https://api.notion.com` | +| `notesPlain` | Vault for which workspace + which databases this integration is connected to | + +The script reads `NOTION_API_KEY` from the environment — it does not call `op` itself. This keeps the script free of `op`-specific dependencies and lets the same script work in CI/CD environments that inject the secret differently (GitHub Actions secrets, Vault, etc.). + +### Fallback: `.env` file (discouraged) + +If you must use a file (e.g. running on a host without 1Password CLI), place it at `~/Project/our-claude-skills/custom-skills/32-notion-writer/code/.env` with: + +``` +NOTION_API_KEY=ntn_xxxxxxxx_paste_your_token_here +``` + +Set the file to mode `600` (`chmod 600 .env`) and never commit it. Add `.env` to `.gitignore` if not already. + +### What NEVER to do + +- ❌ Echo the token to stdout: `echo "$NOTION_API_KEY"` (lands in terminal scrollback + shell history if the var was inline) +- ❌ Pass the token as a CLI argument: `--api-key "ntn_..."` (visible in `ps`, history, telemetry) +- ❌ Commit a `.env` file to git, even briefly (commit history is forever) +- ❌ Paste the token into chat / issues / PRs — assume any pasted secret is compromised within minutes + +### Token rotation + +If a token is exposed (or you reasonably suspect it might be): + +1. Go to → open the integration → **Reset** the secret +2. Update the 1Password item's `api-key` field with the new token +3. Verify with `NOTION_API_KEY="$(op read ...)" python notion_writer.py --test` +4. The old token is invalidated immediately — any leftover scripts using it will fail loudly + ## Commands ### Test Connection