Support stdin pipe for obsidian CLI

Use case or problem

I want to provide larger chunks of content to various Obsidian CLI commands. For example, larger multiline scripts for eval, or larger multi-line content chunks for ‘append’.

Proposed solution

Use nix-style ‘-’ to mean ‘stdin’, e.g.:
echo “helloWorld()” | obsidian eval code=-
echo “Hello World” | obsidian append file=Inbox content=-

Current workaround (optional)

Probably something like:
echo <<-EOF > script.js
helloWorld()
EOF
obsidian eval code=“$(<script.js)”
But this is likely to be full of annoying quote problems.

Related feature requests (optional)

Use case or problem

I’m using the Obsidian 1.12 CLI extensively with AI assistants (Claude + Desktop Commander MCP) to manage my vault programmatically. The CLI works brilliantly for read, search, append, prepend, and property:set.

However, create path="..." content="..." overwrite silently fails when the content contains multiline text passed via shell variables or command substitution. This is because shell argument expansion breaks on special characters (quotes, newlines, brackets, etc.) in longer content.

What works:


# Single-line content — works fine

obsidian create path="test.md" content="Hello World" overwrite

# Append — works fine

obsidian append path="notes.md" content="A new line"

# Read — works perfectly

obsidian read path="notes.md"

What fails silently:


# Multiline content via variable — fails silently (no error, no update)

CONTENT=$(obsidian read path="myfile.md" | sed 's/old/new/')

obsidian create path="myfile.md" content="$CONTENT" overwrite

# Command substitution — same issue

obsidian create path="myfile.md" content="$(cat /tmp/updated.md)" overwrite

The command produces no output and no error, but the file remains unchanged. This makes it impossible to do programmatic find-and-replace or full file rewrites through the CLI alone.

Proposed solution

Support stdin input for the create command, similar to how many Unix tools work:


# Read from stdin via pipe

cat /tmp/updated-file.md | obsidian create path="myfile.md" overwrite --stdin

# Or via heredoc

obsidian create path="myfile.md" overwrite --stdin << 'EOF'

---

title: My Note

---

# Heading

Content with "quotes", (parens), and $pecial chars.

EOF

Alternatively or additionally, support a file source parameter:


# Read content from a file

obsidian create path="myfile.md" overwrite --from-file="/tmp/updated.md"

Both approaches would bypass shell argument escaping issues entirely.

Current workaround

Write the content to /tmp/file.md and use cp to overwrite the vault file directly:


cat > /tmp/myfile.md << 'EOF'

...content...

EOF

cp /tmp/myfile.md "/path/to/vault/myfile.md"

This works but bypasses Obsidian’s internal indexing, meaning links, backlinks, tags, and search may not update immediately until Obsidian detects the filesystem change.

Context

The native CLI is a game-changer for AI-assisted vault management. With read, search, append, and property:set, it covers 90% of use cases perfectly. Adding stdin/file support for create would close the last significant gap and make the CLI a complete solution for programmatic vault operations — no more falling back to direct filesystem writes.

Environment:

  • Obsidian 1.12.1 (macOS, Apple Silicon)

  • Using CLI via MCP server (Desktop Commander) with Claude AI

  • Vault synced via Google Drive

I am experiencing the exact same issue on Windows. Both the create and append commands ought to support STDIN.

Obsidian CLI - Obsidian Help says:
For multiline content use \n for newline. Use \t for tab.

obsidian create name=Note content="# Title\n\nBody text"

That works, but via the TUI, one has to say:

create name=Note content="# Title\\n\\nBody text"

So I suppose the expectation is for user text to modified to escape newlines…

Somewhat Related BR

Here’s a workaround I developed. Calling obsidian-cli to create the file then append a space after content is added appear to trigger indexing the file, which (among other things) keeps the sidebar clean.

#!/usr/bin/env bash
# obsidian-create-from-file.sh
#   creates an Obsidian vault doc from a local file
#
#   - avoids the obsidian-cli `content=` arg-length and multi-line input limit
#   - triggers Obsidian file-watcher to index the new file
#
# This solves my issue where a raw cp trashes the sidebar
#
# Workaround for: 
# https://forum.obsidian.md/t/support-stdin-pipe-for-obsidian-cli/112855
#

set -euo pipefail

VAULT=""
DEST_PATH=""
SRC_FILE=""
CLI="${OBSIDIAN_CLI:-/usr/local/bin/obsidian}"

while [[ $# -gt 0 ]]; do
  case "$1" in
    --vault) VAULT="$2"; shift 2 ;;
    --path)  DEST_PATH="$2"; shift 2 ;;
    --src)   SRC_FILE="$2"; shift 2 ;;
    -h|--help)
      cat <<'EOF'
obsidian-create-from-file.sh -- creates an Obsidian vault doc from a local file

Usage:
  obsidian-create-from-file.sh --vault <name> --path <vault-rel-path> --src <local-file>
EOF
      exit 0
      ;;
    *)
      echo "obsidian-create-from-file: unknown arg: $1" >&2
      exit 2
      ;;
  esac
done

[[ -n "$VAULT" && -n "$DEST_PATH" && -n "$SRC_FILE" ]] || {
  echo "obsidian-create-from-file: --vault, --path, --src all required" >&2
  exit 2
}

[[ -f "$SRC_FILE" ]] || { echo "src file not found: $SRC_FILE" >&2; exit 2; }

# Resolve the vault's filesystem path via the cli (`vault` verb returns vault
# path JSON; we just pull the path field).
vault_root=$("$CLI" vaults verbose \
  | awk -v v="$VAULT" '$1==v {sub(/^[^[:space:]]+[[:space:]]+/,""); sub(/[[:space:]]+$/,""); print; exit}')

[[ -n "$vault_root" ]] || { echo "vault not found in obsidian: $VAULT" >&2; exit 2; }

dest_full="$vault_root/$DEST_PATH"
dest_dir="$(dirname "$dest_full")"

# ensure the target folder exists
mkdir -p "$dest_dir"

# fail cleanly if the file already exists
if [[ -e "$dest_full" ]]; then
  echo "obsidian-create-from-file: refusing to overwrite existing file: $VAULT:$DEST_PATH" >&2
  exit 1
fi

# Step 1: cli create with a stub. Registers with the index
"$CLI" vault="$VAULT" create path="$DEST_PATH" content="."

# Step 2: copy the real content over the stub
cp "$SRC_FILE" "$dest_full"

# Step 3: cli append a space. Forces the running Obsidian to re-scan
"$CLI" vault="$VAULT" append path="$DEST_PATH" content=" "

echo "created: $VAULT:$DEST_PATH ($(wc -l < "$SRC_FILE") lines from $SRC_FILE)"


or see my Gist: /mkosma/a8fecaa3b26d69987ab99d70399572ce