---
name: notion-writer
description: |
  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](#credential-handling) below)
- Target pages/databases must be shared with the integration in Notion (Database/Page → ⋯ → Connections → add integration)

## Quick Start

```bash
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 <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
```bash
python notion_writer.py --test
```

### List Accessible Content
```bash
python notion_writer.py --list
python notion_writer.py --list --filter pages
python notion_writer.py --list --filter databases
```

### Get Page/Database Info
```bash
python notion_writer.py -p PAGE_URL --info
python notion_writer.py -d DATABASE_URL --info
```

### Write to Page
```bash
# 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
```bash
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.

```bash
# 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:
```bash
# 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
```
