External Editors & Baan CLI
The Baan CLI lets you write articles in any Markdown editor — Obsidian, VSCode, Typora, iA Writer — and push them to Baan’s storage with a single command. The CLI communicates with Baan’s CMS API, so it works whether your Baan instance is running locally or on a remote server.
This workflow is ideal if you:
- Already write in Obsidian or have an established Markdown workflow
- Want to version-control your articles with Git
- Prefer writing locally without a browser
- Use Claude Code or another AI tool to help write and publish articles
How It Works
Section titled “How It Works”Your Editor (Obsidian, VSCode, etc.) ↓ writes Markdown files locallyBaan CLI (baan sync / baan posts) ↓ HTTP requests to CMS APIBaan CMS API (/api/cms/v1/*) ↓Storage (GCS / S3 / R2 / Local)Sync is one-way: editor → Baan. The CLI pushes your local files to Baan; it does not pull or modify your local files.
Step 1 — Create a CLI API Key
Section titled “Step 1 — Create a CLI API Key”- Go to Admin → Settings → API Keys
- Click Create Key, give it a name (e.g. “CLI”), and set the type to CLI
- Copy the generated key — it starts with
cms_live_...and is shown only once
CLI keys have cli:full permission, which allows reading and writing (create, update, delete, publish). Headless CMS keys are read-only and will return 403 Forbidden when used with the CLI.
Step 2 — Install the CLI
Section titled “Step 2 — Install the CLI”npm install -g baan-cliStep 3 — Configure with baan init
Section titled “Step 3 — Configure with baan init”baan initThis starts an interactive setup that prompts for your Baan instance URL and API key:
Baan API URL: https://your-blog.com API key: cms_live_xxxxxxxxxxxxxxxxxxxx
Configured: API URL : https://your-blog.com API key : cms_live_xx... Saved to: /Users/you/.baan/config.jsonFor non-interactive use (CI/CD, scripts):
baan init --url https://your-blog.com --key cms_live_xxxxxxxxxxxxxxxxxxxxbaan init is idempotent — running it again overwrites the previous configuration. Configuration is saved to ~/.baan/config.json:
{ "apiUrl": "https://your-blog.com", "apiKey": "cms_live_xxxxxxxxxxxxxxxxxxxx", "defaultLocale": "en"}You can also use environment variables (useful for CI/CD pipelines):
BAAN_API_URL=https://your-blog.comBAAN_API_KEY=cms_live_xxxxxxxxxxxxxxxxxxxxBAAN_LOCALE=enPriority order: flags > environment variables > ~/.baan/config.json
Obsidian Vault Sync
Section titled “Obsidian Vault Sync”baan sync is the recommended workflow for Obsidian users (and anyone with a folder of Markdown files). It tracks which files have changed since the last sync and only sends what’s new or modified.
baan sync ./your-vaultHow Diff Sync Works
Section titled “How Diff Sync Works”On first run, all Markdown files in the vault are synced. On subsequent runs, the CLI computes a hash of each file and compares it against the saved state — only changed files are uploaded.
Sync state is stored in ~/.baan/sync/ as JSON files keyed by the vault path. Each file tracks the article ID, locale, hash, and sync timestamp per Markdown file, plus hashes for each uploaded image.
{ "apiUrl": "https://your-blog.com", "lastSync": "2026-04-05T05:47:53.773Z", "files": { "my-article/my-article.en.md": { "id": "my-article", "locale": "en", "hash": "4ac36b...", "syncedAt": "2026-04-05T05:47:53.683Z", "status": "synced" } }, "images": { "my-article/images/hero.jpg": { "hash": "1ed601...", "syncedAt": "2026-04-05T05:34:06.006Z", "status": "synced" } }}Resetting sync state: You can safely delete these files. The next sync will treat all files as new. If the articles already exist on the server, the CLI detects the conflict and skips them without duplicating — then re-records them in state so subsequent runs track them normally.
rm ~/.baan/sync/*.json # reset all vault statesSync Options
Section titled “Sync Options”baan sync # Use vault from config (no argument needed)baan sync ./vault # Diff sync — only new and changed filesbaan sync ./vault --dry-run # Preview what would change, without sendingbaan sync ./vault --publish # Sync with status: published (default: draft)baan sync ./vault --delete # Also delete from server when removed locallybaan sync ./vault --locale en # Filter to a specific locale (multi-language mode only)baan sync ./vault --concurrency 5 # Parallel uploads (default: 3)If you always sync the same vault, configure it once and omit the path argument from then on:
baan config set-vault ./your-vault # or baan init (prompts for vault path)baan sync # uses configured vaultbaan sync --dry-run # same, with preview--locale behavior differs by mode:
| Mode | --locale en behavior |
|---|---|
| Single-language | Ignored with a warning — Admin’s default locale always applies |
| Multi-language | Acts as a filter — only files whose locale resolves to en are synced (matches both .en.md suffix and locale: en in frontmatter) |
In multi-language mode, omitting --locale syncs all locale files at once.
Dry Run
Section titled “Dry Run”Use --dry-run to preview changes before committing:
baan sync ./vault --dry-run
Syncing: /Users/you/vault Files: 42 total, 3 new, 1 changed, 38 unchanged
New: + my-new-post.md + draft-ideas.md + book-review.md Changed: ~ updated-article.md
Dry run — no changes sent.Image Sync
Section titled “Image Sync”Images referenced in Markdown files are automatically uploaded to Baan’s storage as part of baan sync. You don’t need to upload images separately.
Supported image formats: .jpg, .jpeg, .png, .gif, .webp, .svg
Both reference styles are supported:
 <!-- standard Markdown: relative to the .md file -->![[hero.png]] <!-- Obsidian wikilink: resolved by filename in vault -->![[hero.png|600]] <!-- Obsidian wikilink with width hint -->Images are uploaded to {article-id}/images/{filename} in your Baan storage before the Markdown article is saved, so they are always available when the article goes live.
Like Markdown files, images are diff-tracked — the CLI hashes each image file and skips unchanged ones on subsequent runs.
Syncing: /Users/you/vault Files: 5 total, 1 new, 0 changed, 4 unchanged Images: 2 total, 1 new/changed, 1 unchanged
+ image: my-post/images/hero.png + my-post.md (id: my-post)
Done: 1 synced, 1 image(s) uploadedImage Filename Uniqueness
Section titled “Image Filename Uniqueness”When you use an Obsidian wikilink like ![[hero.png]], the CLI resolves it by searching the entire vault for a file named hero.png. If the same filename exists in more than one folder, the CLI picks whichever it finds first — which may not be the file you intended.
Obsidian itself handles this the same way: it warns you about the ambiguity and asks you to specify the full relative path (e.g., ![[posts/hero.png]]).
Recommended vault layout — images co-located with their article:
vault/├── my-article/│ ├── my-article.md ← article│ └── images/│ └── hero.png ← unique: "hero.png" only here├── another-article/│ ├── another-article.md│ └── images/│ └── cover.png ← different name, no conflict└── shared-images/ └── logo.png ← site-wide asset, unique namePattern to avoid — same filename in multiple folders:
vault/├── 2025/│ └── hero.png ← ⚠️ duplicate├── 2026/│ └── hero.png ← ⚠️ duplicate — CLI picks one arbitrarily└── post.mdIf you need to use images from different folders, make the filenames descriptive and unique:
vault/├── images/│ ├── 2025-conference-hero.png ✅│ ├── 2026-launch-hero.png ✅│ └── team-photo-2026.png ✅└── post.mdMulti-Language Vault Naming
Section titled “Multi-Language Vault Naming”When multi-language mode is enabled in Admin, every Markdown file must declare its locale — either via a filename suffix (.en.md) or via a locale: field in frontmatter. Files with neither are skipped with a warning.
There are two supported styles. Both work with baan sync — choose whichever fits your editor workflow.
Style 1: Locale Suffix in Filename (Baan Standard)
Section titled “Style 1: Locale Suffix in Filename (Baan Standard)”Append the locale code before .md in every filename. The article ID is derived from the folder or file name with the suffix stripped.
One folder per article (recommended):
vault/├── my-article/│ ├── my-article.en.md ← article ID: my-article, locale: en│ ├── my-article.ja.md ← article ID: my-article, locale: ja│ └── images/│ └── hero.jpg ← shared across all locales└── another-article/ ├── another-article.en.md └── another-article.es.mdFlat structure:
vault/├── my-article.en.md├── my-article.ja.md└── another-article.en.mdThe locale code is detected from the suffix — no frontmatter needed. Works well with any editor including VSCode, Typora, and iA Writer.
Style 2: Frontmatter locale: Field (Obsidian-Friendly)
Section titled “Style 2: Frontmatter locale: Field (Obsidian-Friendly)”Use natural, title-based filenames and declare the locale in frontmatter. This style is ideal for Obsidian, where locale suffixes interfere with internal linking.
vault/├── Getting Started with Node.js/│ ├── Getting Started with Node.js.md ← locale: en in frontmatter│ ├── Node.js 入門.md ← locale: ja in frontmatter│ └── images/│ └── hero.jpgEach file includes a locale: field in frontmatter:
---title: Getting Started with Node.jslocale: endate: 2026-04-01---
# Getting Started with Node.js...The article ID is derived from the folder name (or file name when no folder). When files from the same folder share an ID, multiple locale versions are stored under that ID — same as Style 1.
Locale Resolution Priority
Section titled “Locale Resolution Priority”When both a filename suffix and a frontmatter locale: are present, frontmatter takes precedence:
locale:in frontmatter (highest priority)- Filename suffix (
.en.md) - Admin default locale — single-language mode only
Mixing Styles
Section titled “Mixing Styles”Both styles can coexist in the same vault. Each file is evaluated independently.
Default and Custom Exclusions
Section titled “Default and Custom Exclusions”The following files and directories are always excluded from Markdown sync:
| Pattern | Reason |
|---|---|
.obsidian/ | Obsidian workspace metadata |
.trash/ | Obsidian deleted notes |
*.canvas | Obsidian canvas files |
attachments/ | Attachment folder (Markdown files excluded; images here are still uploaded via wikilinks) |
_templates/ | Template notes |
To exclude additional paths, create a .baanignore file in your vault root:
private/work/drafts/*.excalidrawEach line is a pattern. Lines starting with # are comments. Trailing / matches a directory name anywhere in the tree (e.g. private/ excludes any folder named private).
Frontmatter Fallbacks
Section titled “Frontmatter Fallbacks”If a Markdown file is missing required frontmatter fields, baan sync fills them in automatically:
| Missing field | Fallback |
|---|---|
id | Derived from the file path (posts/my-article.md → posts-my-article) |
title | First # H1 heading in the file, or the filename |
date | File modification time (mtime) |
tags | Empty array [] |
category | "Uncategorized" |
Files with aliases in frontmatter use the first alias as the title.
The locale field is not a fallback — it is an explicit declaration used in multi-language mode. See Multi-Language Vault Naming for details.
Checking Sync State
Section titled “Checking Sync State”baan status ./vaultShows which files are synced, changed locally, or have errors.
Writing and Publishing Individual Posts
Section titled “Writing and Publishing Individual Posts”For one-off operations — creating or updating a single article — use the baan posts subcommands directly:
Create a Post from a Markdown File
Section titled “Create a Post from a Markdown File”baan posts create ./my-article.mdbaan posts create ./my-article.md --locale enbaan posts create ./my-article.md --id my-custom-id # override frontmatter idThe article ID is read from the id field in frontmatter. Posts are created as drafts and are not publicly visible until published.
Your Markdown file should include frontmatter with at minimum id and title. See Frontmatter for the complete reference.
Publish / Unpublish
Section titled “Publish / Unpublish”baan posts publish my-article-idbaan posts unpublish my-article-idUpdate an Existing Post
Section titled “Update an Existing Post”baan posts update my-article-id ./my-article.mdbaan posts update my-article-id ./my-article.md --locale enAfter updating, call baan posts publish again if you want the changes to be publicly visible.
Get a Post
Section titled “Get a Post”baan posts get my-article-idbaan posts get my-article-id --jsonbaan posts get my-article-id --no-body # metadata only, skip body contentbaan posts get my-article-id --format markdown # body as Markdown (default: html)List Your Posts
Section titled “List Your Posts”baan posts listbaan posts list --locale en --limit 20 --jsonbaan posts list --sort date:ascDelete a Post
Section titled “Delete a Post”baan posts delete my-article-idbaan posts delete my-article-id --force # skip confirmationAll Commands
Section titled “All Commands”Project
Section titled “Project”baan new <name> # Create a new Baan project from the latest GitHub releasebaan version # Show current and latest Baan version (run from project root)baan update # Update Baan to the latest version (run from project root)Init & Config
Section titled “Init & Config”baan init [--url <url>] [--key <api-key>] [--vault <path>] # Set up API URL, key, and vaultbaan config show # Show current configurationbaan config set-url <url> # Update API URL onlybaan config set-key <api-key> # Update API key onlybaan config set-locale <locale> # Set default localebaan config set-vault <path> # Set default vault directorybaan config reset # Clear all configurationbaan sync [dir] [--dry-run] [--publish] [--delete] [--locale <l>] [--concurrency <n>]baan status [dir]baan posts list [--locale <l>] [--limit <n>] [--offset <n>] [--sort date:desc] [--json]baan posts get <id> [--locale <l>] [--no-body] [--format markdown|html] [--related] [--json]baan posts create <file.md> [--locale <l>] [--id <article-id>] [--json]baan posts update <id> <file.md> [--locale <l>] [--json]baan posts delete <id> [--force] [--json]baan posts publish <id> [--locale <l>]baan posts unpublish <id> [--locale <l>]Categories & Tags
Section titled “Categories & Tags”baan categories list [--locale <l>] [--json]baan categories posts <category> [--locale <l>] [--limit <n>] [--offset <n>] [--json]
baan tags list [--locale <l>] [--json]baan tags posts <tag> [--locale <l>] [--limit <n>] [--offset <n>] [--json]Search
Section titled “Search”baan search "<query>" [--locale <l>] [--category <cat>] [--tags <tag1,tag2>] [--limit <n>] [--json]Common Flags
Section titled “Common Flags”| Flag | Short | Description |
|---|---|---|
--json | -j | Output raw JSON instead of formatted table |
--locale <code> | -l | Locale (e.g. en, ja) |
--dry-run | Preview changes without sending (sync only) | |
--publish | Set status: published on sync (default: draft) | |
--delete | Delete from server when removed locally (sync only) | |
--api-url <url> | Override configured API URL for this run only | |
--api-key <key> | Override configured API key for this run only |
Obsidian Syntax Support
Section titled “Obsidian Syntax Support”Baan understands and converts Obsidian-specific Markdown syntax when syncing:
| Obsidian Syntax | Behavior in Baan |
|---|---|
> [!note] Title | Rendered as a styled callout block |
> [!warning], > [!tip], > [!danger] | Each maps to a styled callout variant |
==highlighted text== | Rendered with <mark> highlight |
%%comment%% | Stripped from output — does not appear on the public site |
![[image.jpg|600]] | Rendered as <img> with width="600" |
[[wikilink]] | Converted to a standard Markdown link |
Typical Workflows
Section titled “Typical Workflows”Vault Sync (Recommended for Obsidian)
Section titled “Vault Sync (Recommended for Obsidian)”# 1. Configure oncebaan init
# 2. Write posts in Obsidian (or any editor)
# 3. Preview what will syncbaan sync ./vault --dry-run
# 4. Sync as drafts firstbaan sync ./vault
# 5. Review in the Baan admin panel, then publishbaan sync ./vault --publishAfter the first sync, subsequent runs only upload changed files.
Individual Post Workflow
Section titled “Individual Post Workflow”# 1. Check what's already in Baanbaan posts list
# 2. Create a single post as a draftbaan posts create ./my-new-article.md
# 3. Review it in the Baan admin panel
# 4. Publish when readybaan posts publish my-new-article-idCommon Scenarios
Section titled “Common Scenarios””I edited an article in Admin. Will baan sync overwrite it?”
Section titled “”I edited an article in Admin. Will baan sync overwrite it?””No. baan sync is diff-based — it only uploads files whose local content has changed since the last sync. If the local file is unchanged, it is skipped entirely. Admin edits are safe.
Admin edits article → baan sync → file is "unchanged" → skipped ✅The CLI does not read from the server. It has no way to detect that the server content has changed — it only compares local file hashes.
”I edited in Admin, but now I want to revert to my local version”
Section titled “”I edited in Admin, but now I want to revert to my local version””Use baan posts update to push a specific file directly, bypassing the hash check:
baan posts update my-article-id ./vault/my-article/My Article.mdbaan posts update my-article-id ./vault/my-article/My Article.md --locale enThis overwrites the server content with your local file unconditionally. Use it when you want to restore local content after Admin edits.
”I edited locally AND in Admin. Which version wins on next sync?”
Section titled “”I edited locally AND in Admin. Which version wins on next sync?””Local wins. When you edit the local file, its hash changes — the CLI detects it as “changed” and uploads it with updatePost, overwriting whatever is on the server.
If you want to preserve both sets of changes, manually merge the Admin content into your local file before syncing.
”I reset sync state (~/.baan/sync/). Will all articles be re-uploaded and overwrite Admin changes?”
Section titled “”I reset sync state (~/.baan/sync/). Will all articles be re-uploaded and overwrite Admin changes?””No. After a state reset:
- All local files appear as “new” to the CLI
- The CLI tries to create each article
- The server responds
409 Conflict— the article already exists - The CLI skips it (does not overwrite) and records it in state
Admin changes are preserved even after a full state reset. The only way to overwrite them is baan posts update or editing the local file and syncing.
”I want to update just one article without running a full sync”
Section titled “”I want to update just one article without running a full sync””baan posts update my-article-id ./vault/my-article/My Article.mdOr, if you only want to push changes to a specific locale:
baan posts update my-article-id ./vault/my-article/My Article.md --locale en“I want to sync only English articles”
Section titled ““I want to sync only English articles””baan sync --locale enIn multi-language mode, this filters to files whose locale resolves to en — both .en.md suffixed files and files with locale: en in frontmatter.
Placing Files Directly in Storage
Section titled “Placing Files Directly in Storage”If you upload Markdown files directly to your storage bucket (GCS, S3, R2, or the local content/ directory) without going through the CLI or Admin editor, the article index will not be updated automatically.
After placing files directly in storage, go to Admin → Settings → System and click Rebuild Index to sync the index with the current state of your storage.
Troubleshooting
Section titled “Troubleshooting”Error Messages
Section titled “Error Messages”The CLI gives actionable messages for common failures:
| Error | Cause | Fix |
|---|---|---|
Connection refused: http://... | Baan server not running | Verify the server is up; run baan config show to check the URL |
Host not found: http://... | Wrong hostname or DNS failure | Check the URL with baan config show or re-run baan init |
Invalid API URL: "..." | Malformed URL saved in config | Run baan init to reconfigure |
Request timed out | Server overloaded or unreachable | Check your network connection and retry |
Connection reset by server | Server closed the connection unexpectedly | Check the Baan server logs |
401 Unauthorized | Invalid or missing API key | Run baan init with the correct key |
403 Forbidden | Key lacks write permission | Use a CLI-type key — read-only CMS keys cannot create or update posts |
404 Not Found | Post or resource does not exist | Check the article ID |
409 Conflict (skipped) | Article already exists — treated as already synced | Normal after resetting sync state; re-recorded automatically |
429 Too Many Requests | Rate limit exceeded (300 requests/hour by default, configurable per key) | Wait and retry; the limit resets hourly |
500 Internal Server Error | Server-side error | Check the Baan server logs |
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
0 | Success |
1 | API error, network error, or file error |
2 | Not configured — API URL or key is missing. Run baan init |
Checking Your Configuration
Section titled “Checking Your Configuration”baan config showShows the current API URL, API key (first 8 characters), and default locale.
Claude Code Integration
Section titled “Claude Code Integration”If you use Claude Code, you can manage your Baan blog in natural language — no need to remember command syntax.
Copy the Baan skill into your vault (or any writing directory you open with Claude Code):
mkdir -p .claude/skills/baancurl -o .claude/skills/baan/SKILL.md \ https://raw.githubusercontent.com/baan-press/baan-cli/main/templates/.claude/skills/baan/SKILL.mdOpen your vault in Claude Code and talk to it naturally:
| What you say | What happens |
|---|---|
| ”sync して” | Runs dry-run, shows changes, asks for confirmation, then syncs |
| ”記事一覧を見せて” | Lists all posts as a formatted table |
| ”この記事を公開して” | Publishes the specified post |
| ”下書きに戻して” | Unpublishes the post |
| ”vault を同期して公開まで一気にやって” | Syncs and publishes in one flow |
The skill handles the safe workflow automatically — dry-run before sync, confirmation before delete, and draft reminders after create/update.