The 02-notion-writer dir was renumbered to 32-notion-writer, but the skill's SKILL.md (both code + desktop variants) still pointed venv/cd paths at the nonexistent 02 dir, and the slash-command file had a ~/Projects (should be ~/Project) typo. Fixes invocation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.2 KiB
name, description
| name | description |
|---|---|
| notion-writer | Markdown to Notion page writer with database row creation support. Triggers: write to Notion, export to Notion, push content, create Notion page. |
Notion Writer Skill
Push markdown content to Notion pages or databases via Claude Code.
Prerequisites
- Python virtual environment at
~/Project/our-claude-skills/custom-skills/32-notion-writer/code/scripts/venv - Notion integration token (preferred: stored in 1Password — see Credential handling below)
- Target pages/databases must be shared with the integration in Notion (Database/Page → ⋯ → Connections → add integration)
Quick Start
cd ~/Project/our-claude-skills/custom-skills/32-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.
# 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:
(
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 inps, history, telemetry) - ❌ Commit a
.envfile 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):
- Go to https://www.notion.so/my-integrations → open the integration → Reset the secret
- Update the 1Password item's
api-keyfield with the new token - Verify with
NOTION_API_KEY="$(op read ...)" python notion_writer.py --test - The old token is invalidated immediately — any leftover scripts using it will fail loudly
Commands
Test Connection
python notion_writer.py --test
List Accessible Content
python notion_writer.py --list
python notion_writer.py --list --filter pages
python notion_writer.py --list --filter databases
Get Page/Database Info
python notion_writer.py -p PAGE_URL --info
python notion_writer.py -d DATABASE_URL --info
Write to Page
# Append content
python notion_writer.py -p PAGE_URL -f content.md
# Replace content
python notion_writer.py -p PAGE_URL -f content.md --replace
# From stdin
cat report.md | python notion_writer.py -p PAGE_URL --stdin
Create Database Row
python notion_writer.py -d DATABASE_URL -t "Entry Title" -f content.md
Supported Markdown
| Markdown | Notion Block |
|---|---|
# Heading |
Heading 1 |
## Heading |
Heading 2 |
### Heading |
Heading 3 |
- item |
Bulleted list |
1. item |
Numbered list |
- [ ] task |
To-do (unchecked) |
- [x] task |
To-do (checked) |
> quote |
Quote |
```code``` |
Code block |
--- |
Divider |
| Paragraphs | Paragraph |
Engines and image uploads
Two write engines via --engine {blocks,markdown} (default: blocks).
The blocks engine (default) converts markdown locally to Notion block objects. Local images () are auto-uploaded via the ntn CLI and embedded at their original position in the page. Requires ntn installed and ntn login.
The markdown engine (--engine markdown) posts the document through Notion's native enhanced-markdown API (Notion-Version: 2026-03-11, set automatically; override with --notion-version). The skill's authoring dialect — GitHub alerts ([!NOTE]), Pandoc columns (::: columns), <details> toggles, and @[mention] — is auto-translated before posting. Note: local images are appended at the end of the page rather than inline with this engine; use --engine blocks when image position matters. Pass --allow-deleting-content when --replace needs to remove child pages or databases.
# Markdown engine — create a DB row from a doc with callouts or columns
python notion_writer.py -d DB_URL -t "Notes" --engine markdown -f notes.md
Workflow Example
Integrate with Jamie YouTube Manager to log video info:
# Check video and save to markdown
python jamie_youtube_api_test.py VIDEO_URL
# Write to Notion
python notion_writer.py -p LOG_PAGE_URL -f output/video_status.md