Adopt: directory keeps its NN- ordering prefix; skill `name:` is the clean form without it (dir 16-seo-schema-validator → name: seo-schema-validator). Nicer to invoke, matches the original desktop/SKILL.md names, still globally unique. - 71 root SKILL.md: name: NN-foo → name: foo (flat skills + reference-curator suite). Plugins (mac-optimizer/multi-agent-guide/dintel-bootstrap) already clean; 95 already clean. - scripts/migrate_skill_root.py: derive name = dirname minus NN- prefix (skill_name()). - CLAUDE.md + SKILL-MIGRATION-GUIDE.md: document the dir-prefix / clean-name convention. verify_skills.py: 0 name collisions across all renamed skills. (The ~/.claude/skills symlinks were re-pointed to the clean names separately — filesystem only.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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 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 |
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