Skip to content

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

Your Editor (Obsidian, VSCode, etc.)
↓ writes Markdown files locally
Baan CLI (baan sync / baan posts)
↓ HTTP requests to CMS API
Baan 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.


  1. Go to Admin → Settings → API Keys
  2. Click Create Key, give it a name (e.g. “CLI”), and set the type to CLI
  3. 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.

Terminal window
npm install -g baan-cli
Terminal window
baan init

This 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.json

For non-interactive use (CI/CD, scripts):

Terminal window
baan init --url https://your-blog.com --key cms_live_xxxxxxxxxxxxxxxxxxxx

baan 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.com
BAAN_API_KEY=cms_live_xxxxxxxxxxxxxxxxxxxx
BAAN_LOCALE=en

Priority order: flags > environment variables > ~/.baan/config.json


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.

Terminal window
baan sync ./your-vault

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.

Terminal window
rm ~/.baan/sync/*.json # reset all vault states
Terminal window
baan sync # Use vault from config (no argument needed)
baan sync ./vault # Diff sync — only new and changed files
baan sync ./vault --dry-run # Preview what would change, without sending
baan sync ./vault --publish # Sync with status: published (default: draft)
baan sync ./vault --delete # Also delete from server when removed locally
baan 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:

Terminal window
baan config set-vault ./your-vault # or baan init (prompts for vault path)
baan sync # uses configured vault
baan sync --dry-run # same, with preview

--locale behavior differs by mode:

Mode--locale en behavior
Single-languageIgnored with a warning — Admin’s default locale always applies
Multi-languageActs 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.

Use --dry-run to preview changes before committing:

Terminal window
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.

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:

![hero image](images/hero.png) <!-- 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) uploaded

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 name

Pattern to avoid — same filename in multiple folders:

vault/
├── 2025/
│ └── hero.png ← ⚠️ duplicate
├── 2026/
│ └── hero.png ← ⚠️ duplicate — CLI picks one arbitrarily
└── post.md

If 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.md

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.md

Flat structure:

vault/
├── my-article.en.md
├── my-article.ja.md
└── another-article.en.md

The 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.jpg

Each file includes a locale: field in frontmatter:

---
title: Getting Started with Node.js
locale: en
date: 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.

When both a filename suffix and a frontmatter locale: are present, frontmatter takes precedence:

  1. locale: in frontmatter (highest priority)
  2. Filename suffix (.en.md)
  3. Admin default locale — single-language mode only

Both styles can coexist in the same vault. Each file is evaluated independently.

The following files and directories are always excluded from Markdown sync:

PatternReason
.obsidian/Obsidian workspace metadata
.trash/Obsidian deleted notes
*.canvasObsidian 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:

.baanignore
private/
work/
drafts/
*.excalidraw

Each 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).

If a Markdown file is missing required frontmatter fields, baan sync fills them in automatically:

Missing fieldFallback
idDerived from the file path (posts/my-article.mdposts-my-article)
titleFirst # H1 heading in the file, or the filename
dateFile modification time (mtime)
tagsEmpty 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.


Terminal window
baan status ./vault

Shows which files are synced, changed locally, or have errors.


For one-off operations — creating or updating a single article — use the baan posts subcommands directly:

Terminal window
baan posts create ./my-article.md
baan posts create ./my-article.md --locale en
baan posts create ./my-article.md --id my-custom-id # override frontmatter id

The 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.

Terminal window
baan posts publish my-article-id
baan posts unpublish my-article-id
Terminal window
baan posts update my-article-id ./my-article.md
baan posts update my-article-id ./my-article.md --locale en

After updating, call baan posts publish again if you want the changes to be publicly visible.

Terminal window
baan posts get my-article-id
baan posts get my-article-id --json
baan posts get my-article-id --no-body # metadata only, skip body content
baan posts get my-article-id --format markdown # body as Markdown (default: html)
Terminal window
baan posts list
baan posts list --locale en --limit 20 --json
baan posts list --sort date:asc
Terminal window
baan posts delete my-article-id
baan posts delete my-article-id --force # skip confirmation

Terminal window
baan new <name> # Create a new Baan project from the latest GitHub release
baan version # Show current and latest Baan version (run from project root)
baan update # Update Baan to the latest version (run from project root)
Terminal window
baan init [--url <url>] [--key <api-key>] [--vault <path>] # Set up API URL, key, and vault
baan config show # Show current configuration
baan config set-url <url> # Update API URL only
baan config set-key <api-key> # Update API key only
baan config set-locale <locale> # Set default locale
baan config set-vault <path> # Set default vault directory
baan config reset # Clear all configuration
Terminal window
baan sync [dir] [--dry-run] [--publish] [--delete] [--locale <l>] [--concurrency <n>]
baan status [dir]
Terminal window
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>]
Terminal window
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]
Terminal window
baan search "<query>" [--locale <l>] [--category <cat>] [--tags <tag1,tag2>] [--limit <n>] [--json]
FlagShortDescription
--json-jOutput raw JSON instead of formatted table
--locale <code>-lLocale (e.g. en, ja)
--dry-runPreview changes without sending (sync only)
--publishSet status: published on sync (default: draft)
--deleteDelete 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

Baan understands and converts Obsidian-specific Markdown syntax when syncing:

Obsidian SyntaxBehavior in Baan
> [!note] TitleRendered 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

Terminal window
# 1. Configure once
baan init
# 2. Write posts in Obsidian (or any editor)
# 3. Preview what will sync
baan sync ./vault --dry-run
# 4. Sync as drafts first
baan sync ./vault
# 5. Review in the Baan admin panel, then publish
baan sync ./vault --publish

After the first sync, subsequent runs only upload changed files.

Terminal window
# 1. Check what's already in Baan
baan posts list
# 2. Create a single post as a draft
baan posts create ./my-new-article.md
# 3. Review it in the Baan admin panel
# 4. Publish when ready
baan posts publish my-new-article-id

”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:

Terminal window
baan posts update my-article-id ./vault/my-article/My Article.md
baan posts update my-article-id ./vault/my-article/My Article.md --locale en

This 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:

  1. All local files appear as “new” to the CLI
  2. The CLI tries to create each article
  3. The server responds 409 Conflict — the article already exists
  4. 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””
Terminal window
baan posts update my-article-id ./vault/my-article/My Article.md

Or, if you only want to push changes to a specific locale:

Terminal window
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””
Terminal window
baan sync --locale en

In multi-language mode, this filters to files whose locale resolves to en — both .en.md suffixed files and files with locale: en in frontmatter.


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.


The CLI gives actionable messages for common failures:

ErrorCauseFix
Connection refused: http://...Baan server not runningVerify the server is up; run baan config show to check the URL
Host not found: http://...Wrong hostname or DNS failureCheck the URL with baan config show or re-run baan init
Invalid API URL: "..."Malformed URL saved in configRun baan init to reconfigure
Request timed outServer overloaded or unreachableCheck your network connection and retry
Connection reset by serverServer closed the connection unexpectedlyCheck the Baan server logs
401 UnauthorizedInvalid or missing API keyRun baan init with the correct key
403 ForbiddenKey lacks write permissionUse a CLI-type key — read-only CMS keys cannot create or update posts
404 Not FoundPost or resource does not existCheck the article ID
409 Conflict (skipped)Article already exists — treated as already syncedNormal after resetting sync state; re-recorded automatically
429 Too Many RequestsRate limit exceeded (300 requests/hour by default, configurable per key)Wait and retry; the limit resets hourly
500 Internal Server ErrorServer-side errorCheck the Baan server logs
CodeMeaning
0Success
1API error, network error, or file error
2Not configured — API URL or key is missing. Run baan init
Terminal window
baan config show

Shows the current API URL, API key (first 8 characters), and default locale.


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):

Terminal window
mkdir -p .claude/skills/baan
curl -o .claude/skills/baan/SKILL.md \
https://raw.githubusercontent.com/baan-press/baan-cli/main/templates/.claude/skills/baan/SKILL.md

Open your vault in Claude Code and talk to it naturally:

What you sayWhat 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.