Files
our-claude-skills/custom-skills/32-notion-writer/SKILL.md
2026-06-27 11:13:31 +09:00

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/02-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/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.

# 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 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 https://www.notion.so/my-integrations → 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

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 (![alt](./file.png)) 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