notionctl
v0.1.4
Published
Security-auditable, zero-dependency command-line interface for Notion, designed to be driven by AI coding agents via shell invocations.
Maintainers
Readme
notionctl
The security-first CLI for Notion. Zero dependencies. Fully auditable. Built for AI agents and humans alike.
npm install -g notionctlRequires Node 18+.
Quick Start
# Option A: OAuth browser login (recommended)
notionctl auth login --client-id <id> --client-secret <secret>
# Option B: Paste an integration token
notionctl auth set
# Option C: Environment variable
export NOTION_TOKEN=ntn_...
# Verify
notionctl whoami
# Go
notionctl search "Q2 Roadmap"
notionctl page get <url-or-id>
notionctl page sync ./prd.md --parent <page-id>Why notionctl
AI coding agents (Claude Code, Copilot, Cursor) work best when they can read and write your team's Notion knowledge base. MCP servers bridge this gap, but require persistent processes with broad privileges and a larger attack surface.
notionctl is the alternative: one shell command per operation, shell-logged, --dry-run-able, and auditable line by line. Every action an agent takes is a visible terminal invocation.
notionctl vs MCP
| | notionctl | Notion MCP server |
|---|---|---|
| Token cost | One shell command + one response per operation | Tool schemas, JSON-RPC framing, and capability negotiation all consume context tokens |
| Latency | Single process: spawn → HTTP call → exit | Persistent server + JSON-RPC round-trip per call |
| Agent context | Agent sees notionctl page get <id> — one line | Agent loads full tool schema list into context window on every session |
| Setup | npm i -g notionctl + one token | Server process, config file, client wiring |
| Auditability | Every call is a shell command in your terminal log | Operations happen inside an opaque server process |
| Security surface | Zero deps, single network file, auditable in an afternoon | Server framework, transitive deps, persistent token in memory |
Security Model
notionctl was designed so that a security team can audit the entire tool in an afternoon.
- Zero runtime dependencies. The entire codebase is hand-written TypeScript compiled to ES modules. No
node_modules. No transitive supply chain risk. - Single network file. All outbound traffic goes through
src/http.ts, hardcoded tohttps://api.notion.com. No--api-baseflag, no proxy support, no way to redirect the token. - Single secrets file. Token access is isolated to
src/auth.ts. Tokens live inNOTION_TOKENor a mode-0600 config file. Tokens never appear in logs, errors, or stdout. - Automated security tests.
npm run test:securityruns grep-based source tree assertions that encode these invariants. They fail the build on drift:- Only
http.tscallsfetch() - Only
auth.tsreads the config path - No hardcoded token patterns in source
- No non-Node-builtin imports
- Only
api.notion.comURLs in source
- Only
- Dry-run everything. Every write command supports
--dry-runto preview the payload without sending. - Content-hashed sync.
page syncuses SHA-256 frontmatter hashing with drift detection to prevent accidental overwrites. - No telemetry. Zero outbound traffic beyond
api.notion.com. Provable:grep -r "https://" src/.
Command Surface
39 commands across 8 categories. Full reference: docs/COMMANDS.md.
Pages
notionctl page get <id> # Read as Markdown
notionctl page create --parent <id> --title "X" # Create from Markdown
notionctl page sync ./doc.md --parent <id> # Bidirectional sync
notionctl page find-replace <id> --find "v1" --replace "v2"
notionctl page duplicate <id>
notionctl page move <id> --to <parent-id>
notionctl page open <id> # Open in browser
notionctl page restore <id> # Undelete
notionctl page delete <id> --yesDatabases
notionctl db create --parent <id> --title "Tasks" \
--prop Status=select:Todo,Doing,Done --prop Due=date
notionctl db query <id> --filter "Status=Done" --sort "Due:desc"
notionctl db schema <id>
notionctl db row create <id> --prop "Name=Ship v2" --prop "Status=Todo"
notionctl db row update <id> --prop "Status=Done"Filter DSL supports =, >, <, >=, <= across select, number, date, text, checkbox, and multi_select types. For complex filters: --filter-json @filter.json.
Blocks, Files, Comments, Users
notionctl block children <id> --recursive # Full page subtree
notionctl block append <id> --from patch.md --after <block-id>
notionctl file upload ./screenshot.png --parent <page-id>
notionctl comment add <page-id> --text "LGTM"
notionctl user listAuth
notionctl auth login --client-id <id> --client-secret <secret> # OAuth
notionctl auth set [--profile staging] # Token from stdin
notionctl auth status # Verify token
notionctl auth doctor # Full diagnosticsEscape Hatch
Any Notion REST endpoint, with the CLI's auth and retry behavior:
notionctl api GET /users/me
notionctl api POST /databases/<id>/query --body @filter.jsonMarkdown Engine
Bidirectional Markdown conversion with full fidelity:
Read: headings, paragraphs, bullet/numbered/to-do lists (nested), code blocks, tables (GFM), quotes, callouts, toggles, dividers, images, equations, bold, italic, strikethrough, inline code, links.
Write: all of the above. Nested lists use 2-space indentation and produce the corresponding nested block tree in Notion.
Sync with Drift Detection
notionctl page sync ./prd.md --parent <id> # Creates page, writes notion_id to frontmatter
# ...edit locally...
notionctl page sync ./prd.md # Updates only if local content changedEach sync stores a SHA-256 content hash and timestamp in the file's YAML frontmatter. On subsequent syncs, if someone edited the page in Notion after your last sync, notionctl refuses to overwrite and tells you to fetch the remote version first. Override with --force.
Output Formats
| Context | Default | Override |
|---------|---------|----------|
| TTY (interactive) | Human-friendly (table, Markdown) | --format json |
| Piped (scripts) | JSON | --format table |
All commands support --format md|json|table|csv.
Testing
731 automated tests. Zero test framework dependencies (uses Node.js built-in node:test).
npm test # Full suite
npm run test:security # Security invariant checksFull test coverage breakdown: docs/TESTING.md.
Inspired By
notionctl was written from scratch, but drew on patterns from:
- 4ier/notion-cli -- command taxonomy, filter DSL,
apiescape hatch - Coastal-Programs/notion-cli -- structured error model
- lox/notion-cli --
page syncwith frontmatter ID
License
MIT. See LICENSE.
Security Disclosure
See SECURITY.md for the responsible disclosure process.
