Files
our-claude-skills/custom-skills/32-notion-writer/desktop/SKILL.md
Andrew Yim e1bd799672 docs(notion-writer): document 1Password credential handling, deprecate .env
Plaintext .env storage of NOTION_API_KEY is unnecessary risk — Notion
integration tokens grant write access to every connected page/database
and have no expiry. Document the 1Password CLI fetch pattern as the
preferred path; .env becomes a discouraged fallback for environments
without `op`.

SKILL.md adds a Credential handling section covering:
- One-shot inline fetch via `op read 'op://Development/Notion - Claude Agent/api-key'`
- Subshell-scoped export pattern for repeated use without shell-history exposure
- 1Password field-name conventions (api-key for the token)
- Anti-patterns to avoid (echo, CLI args, git commits, chat paste)
- Token rotation procedure

notion_writer.py error message updated to point at the 1Password recipe
when NOTION_API_KEY is missing, instead of telling users to create a .env.

Tested end-to-end: pushed a 22 KB markdown report (16 tables, 120 nested
checkboxes) using the inline 1Password fetch. Bot identity verified, all
markdown blocks rendered as native Notion blocks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 19:14:48 +09:00

5.1 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

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