JSON Mode adds machine-readable output to all polis CLI commands via a global --json flag. The flag can be placed at the start or end of the command:
polis --json post article.md # Flag at start
polis post article.md --json # Flag at end (also works)This enables:
- Scriptable workflows - Chain commands together programmatically
- Error handling - Structured error codes for better automation
- Testing - Validate command output with JSON parsers
- Integration - Connect polis with other tools and pipelines
When --json is enabled:
- Output is valid JSON to stdout (success) or stderr (errors)
- Interactive prompts are auto-skipped with logged defaults
- ANSI color codes are disabled
- Exit codes indicate success (0) or failure (1)
# Publish multiple posts and collect hashes
for post in posts/*.md; do
result=$(polis --json post "$post")
hash=$(echo "$result" | jq -r '.data.content_hash')
echo "$post: $hash"
done# Auto-approve all pending blessing requests
requests=$(polis --json blessing requests)
echo "$requests" | jq -r '.data.requests[].id' | while read id; do
polis --json blessing grant "$id"
done# Publish and verify in CI pipeline
if ! result=$(polis --json post article.md 2>&1); then
error_code=$(echo "$result" | jq -r '.error.code')
echo "::error::Publication failed: $error_code"
exit 1
fi
hash=$(echo "$result" | jq -r '.data.content_hash')
echo "::set-output name=hash::$hash"# Extract and validate all metadata
polis --json init > init-result.json
jq '.data.key_paths' init-result.json
# Verify index integrity
polis --json rebuild | jq '.data | {posts, comments, index_path}'# Read URLs from file and comment on each
cat post-urls.txt | while read url; do
echo "Great post!" | polis --json comment "$url"
doneWhen interactive prompts are auto-skipped in JSON mode, these defaults are used:
| Command | Prompt | Default Value | Logged Message |
|---|---|---|---|
comment |
"Comment URL:" | Derived from POLIS_BASE_URL + canonical path |
[default] Using comment URL from POLIS_BASE_URL + derived path |
follow |
"Grant all pending blessings? (y/N):" | y (yes) |
[default] Auto-confirming: yes |
unfollow |
"Deny all pending blessings? (y/N):" | y (yes) |
[default] Auto-confirming: yes |
Note: Default messages are written to stderr so they don't interfere with JSON output on stdout.
{
"status": "success",
"command": "command-name",
"data": {
// Command-specific fields
}
}{
"status": "error",
"command": "command-name",
"error": {
"code": "ERROR_CODE",
"message": "Human-readable error message",
"details": {}
}
}{
"status": "success",
"command": "init",
"data": {
"directories_created": [".polis/keys", "posts", "comments", "metadata"],
"files_created": [
".well-known/polis",
"metadata/public.jsonl",
"metadata/blessed-comments.json",
"metadata/following.json"
],
"key_paths": {
"private": ".polis/keys/id_ed25519",
"public": ".polis/keys/id_ed25519.pub"
}
}
}Returns complete system information including site details, versions, configuration, keys, and discovery status.
{
"status": "success",
"command": "about",
"data": {
"site": {
"url": "https://example.com",
"title": "My Blog"
},
"versions": {
"cli": "0.65.0",
"well_known_polis": "1.0",
"following": "1.0",
"blessed_comments": "1.0",
"manifest": "1.0"
},
"configuration": {
"directories": {
"keys": ".polis/keys",
"posts": "posts",
"comments": "comments",
"snippets": "snippets",
"versions": ".versions"
},
"files": {
"public_index": "metadata/public.jsonl",
"blessed_comments": "metadata/blessed-comments.json",
"following": "metadata/following.json",
"manifest": "metadata/manifest.json"
}
},
"keys": {
"status": "initialized",
"fingerprint": "SHA256:abc123...",
"public_key_path": ".polis/keys/id_ed25519.pub"
},
"discovery": {
"service_url": "https://ds.polis.pub",
"api_key_set": true,
"registration": {
"status": "registered",
"registry_url": "https://...",
"registered_at": "2026-01-10T12:00:00Z"
}
},
"project": {
"repository": "https://github.com/vdibart/polis",
"license": "AGPL-3.0"
}
}
}Note: Sensitive values like the API key show api_key_set: true/false instead of the actual value.
{
"status": "success",
"command": "post",
"data": {
"file_path": "posts/2026/01/my-post.md",
"content_hash": "sha256:abc123...",
"timestamp": "2026-01-15T12:00:00Z",
"signature": "-----BEGIN SSH SIGNATURE-----...",
"canonical_url": "https://example.com/posts/2026/01/my-post.md"
}
}{
"status": "success",
"command": "republish",
"data": {
"file_path": "posts/2026/01/my-post.md",
"previous_version": "sha256:abc123...",
"new_version": "sha256:def456...",
"timestamp": "2026-01-15T14:00:00Z",
"signature": "-----BEGIN SSH SIGNATURE-----..."
}
}{
"status": "success",
"command": "preview",
"data": {
"url": "https://alice.com/posts/2026/01/hello.md",
"type": "post",
"title": "Hello World",
"published": "2026-01-15T12:00:00Z",
"current_version": "sha256:abc123...",
"generator": "polis-cli-go/0.65.0",
"in_reply_to": null,
"author": "alice@example.com",
"signature": {
"status": "valid",
"message": "Good signature from alice@example.com"
},
"hash": {
"status": "valid"
},
"validation_issues": [],
"body": "# Hello World\n\nThis is my first post..."
}
}{
"status": "success",
"command": "comment",
"data": {
"file_path": "comments/2026/01/reply.md",
"content_hash": "sha256:def456...",
"in_reply_to": "https://bob.com/posts/original.md",
"timestamp": "2026-01-15T12:30:00Z",
"beseech_status": "pending"
}
}{
"status": "success",
"command": "blessing-sync",
"data": {
"synced_count": 3
}
}{
"status": "success",
"command": "blessing-requests",
"data": {
"count": 3,
"requests": [
{
"id": 1,
"comment_url": "https://alice.com/comments/reply.md",
"author": "alice@example.com",
"timestamp": "2025-01-15T12:00:00Z"
}
]
}
}{
"status": "success",
"command": "blessing-grant",
"data": {
"comment_version": "sha256:f4bac5d0...",
"comment_url": "https://alice.com/comments/reply.md",
"blessed_at": "2026-01-15T13:00:00Z",
"blessed_by": "bob@example.com"
}
}{
"status": "success",
"command": "blessing-deny",
"data": {
"comment_version": "sha256:f4bac5d0...",
"comment_url": "https://alice.com/comments/reply.md",
"denied_at": "2026-01-15T13:00:00Z",
"denied_by": "bob@example.com"
}
}Re-request blessing for a comment by its content hash (short form like abc123-def456 or full SHA256).
{
"status": "success",
"command": "blessing-beseech",
"data": {
"comment_url": "https://alice.com/comments/reply.md",
"comment_version": "sha256:abc123...",
"in_reply_to": "https://bob.com/posts/original.md",
"discovery_response": {
"success": true,
"message": "Beseech request recorded",
"status": "pending"
}
}
}If already blessed:
{
"status": "success",
"command": "blessing-beseech",
"data": {
"status": "already_blessed",
"comment_version": "sha256:abc123..."
}
}{
"status": "success",
"command": "follow",
"data": {
"author_url": "https://alice.com",
"author_email": "alice@example.com",
"comments_found": 5,
"comments_blessed": 5,
"added_to_following": true
}
}Similar structure to follow, with removed_from_following and comments_denied fields.
{
"status": "success",
"command": "rebuild",
"data": {
"posts_indexed": 12,
"comments_indexed": 34,
"index_path": "metadata/public.jsonl"
}
}{
"status": "success",
"command": "render",
"data": {
"posts_rendered": 5,
"posts_skipped": 12,
"comments_rendered": 3,
"comments_skipped": 8,
"index_generated": true
}
}Returns posts and comments grouped from the public index.
{
"version": "0.16.0",
"posts": [
{
"title": "My First Post",
"url": "https://example.com/posts/2026/01/my-first-post.md",
"published": "2026-01-15T12:00:00Z",
"version": "sha256:abc123..."
}
],
"comments": [
{
"title": "Re: Their Post",
"url": "https://example.com/comments/2026/01/reply.md",
"published": "2026-01-15T14:00:00Z",
"in_reply_to": "https://other.com/posts/their-post.md",
"version": "sha256:def456..."
}
]
}Note: This command outputs JSON directly (not wrapped in success envelope) for compatibility with JSONL tooling.
{
"status": "success",
"command": "notifications",
"data": {
"pending_blessings": [
{
"id": 42,
"comment_url": "https://alice.com/comments/reply.md",
"author": "alice@example.com",
"in_reply_to": "https://example.com/posts/my-post.md",
"timestamp": "2026-01-15T12:00:00Z"
}
],
"domain_migrations": [
{
"old_domain": "old-site.com",
"new_domain": "new-site.com",
"migrated_at": "2026-01-14T10:00:00Z"
}
]
}
}{
"status": "success",
"command": "migrate",
"data": {
"old_domain": "old-site.com",
"new_domain": "new-site.com",
"posts_updated": 12,
"comments_updated": 5,
"database_updated": true,
"database_rows": 17
}
}{
"status": "success",
"command": "rotate-key",
"data": {
"posts_resigned": 12,
"posts_failed": 0,
"comments_resigned": 5,
"comments_failed": 0,
"old_key": "archived",
"new_key_fingerprint": "SHA256:abc123..."
}
}The old_key field is either "archived" or "deleted" depending on whether --delete-old-key was used.
{
"status": "success",
"command": "tag-list",
"data": {
"count": 3,
"tags": [
{
"name": "favorite",
"targets": [
"https://alice.com/posts/20260301/on-gardens.md",
"https://bob.com/posts/20260215/hello.md"
]
}
]
}
}{
"status": "success",
"command": "tag-apply",
"data": {
"tag": "favorite",
"target": "https://alice.com/posts/20260301/on-gardens.md",
"applied_at": "2026-03-19T12:00:00Z"
}
}{
"status": "success",
"command": "tag-remove",
"data": {
"tag": "favorite",
"target": "https://alice.com/posts/20260301/on-gardens.md",
"removed_at": "2026-03-19T12:05:00Z"
}
}{
"status": "success",
"command": "tag-delete",
"data": {
"tag": "old-topic",
"associations_removed": 5,
"deleted_at": "2026-03-19T12:10:00Z"
}
}| Code | Description | Common Causes | Example |
|---|---|---|---|
FILE_NOT_FOUND |
Required file doesn't exist | Missing input file, file deleted | polis --json post missing.md |
INVALID_INPUT |
User input validation failed | Missing argument, invalid format | polis --json post (no file) |
API_ERROR |
Remote API call failed | Network issue, endpoint down, HTTP error | Discovery service unreachable |
SIGNATURE_ERROR |
Signature verification failed | Invalid key, corrupted file, wrong algorithm | Signature mismatch in beseech |
MISSING_DEPENDENCY |
Required tool not found | jq, ssh-keygen, git not installed | command not found: jq |
PERMISSION_ERROR |
File/directory permission denied | Read-only filesystem, insufficient permissions | Cannot write to .polis/ |
INVALID_STATE |
Operation not valid in current state | Polis not initialized, file already published | polis post before polis init |
{
"status": "error",
"command": "post",
"error": {
"code": "FILE_NOT_FOUND",
"message": "File not found: article.md",
"details": {}
}
}-
--quietflag - Suppress[default]log messagespolis --json --quiet publish test.md # No stderr output, only JSON result -
Alternative output formats
polis --format=yaml publish test.md polis --format=toml publish test.md
-
JSON formatting options
polis --json --pretty publish test.md # Pretty-printed polis --json --compact publish test.md # Minified (default)
-
Batch mode
# Process multiple operations from JSON input cat operations.json | polis --json --batch
-
Webhook integration
# Post results to webhook polis --json --webhook=https://api.example.com/hooks publish test.md -
Progress tracking for long operations
{ "status": "in_progress", "command": "follow", "progress": { "current": 3, "total": 10, "message": "Blessing comment 3 of 10" } }
--dry-runflag for testing without side effects--verboseflag for detailed operation logs in JSON- Structured logging with log levels (DEBUG, INFO, WARN, ERROR)
- Machine-readable timestamps (ISO 8601 vs Unix epoch option)
# Publish a post and capture result
result=$(polis --json post my-post.md)
echo "$result" | jq -r '.data.content_hash'# Publish and beseech in one script
publish_result=$(polis --json post comment.md)
comment_url=$(echo "$publish_result" | jq -r '.data.canonical_url')
polis --json beseech "$comment_url"# Check blessing requests and auto-grant first one
requests=$(polis --json blessing requests)
first_id=$(echo "$requests" | jq -r '.data.requests[0].id')
polis --json blessing grant "$first_id"# Proper error handling with structured codes
if ! result=$(polis --json post test.md 2>&1); then
error_code=$(echo "$result" | jq -r '.error.code')
error_msg=$(echo "$result" | jq -r '.error.message')
case "$error_code" in
FILE_NOT_FOUND)
echo "Error: File doesn't exist"
exit 2
;;
INVALID_STATE)
echo "Error: Run 'polis init' first"
exit 3
;;
*)
echo "Error: $error_msg"
exit 1
;;
esac
fi
echo "Success! Hash: $(echo "$result" | jq -r '.data.content_hash')"# Process all markdown files in a directory
for file in posts/*.md; do
if result=$(polis --json post "$file" 2>&1); then
hash=$(echo "$result" | jq -r '.data.content_hash')
echo "✓ $file → $hash"
else
error=$(echo "$result" | jq -r '.error.code')
echo "✗ $file → $error"
fi
done# Extract specific fields from complex responses
polis --json follow https://alice.com | \
jq '{
author: .data.author_email,
blessed: .data.comments_blessed,
success: (.data.comments_blessed > 0)
}'# Only proceed if blessing requests exist
count=$(polis --json blessing requests | jq -r '.data.count')
if [ "$count" -gt 0 ]; then
echo "Processing $count blessing requests..."
# Auto-approve logic here
else
echo "No pending requests"
fiBefore (human-readable output):
polis post article.md
# Output: [✓] Published posts/2025/01/article.mdAfter (JSON mode):
polis --json post article.md | jq -r '.data.file_path'
# Output: posts/2025/01/article.md- Default behavior unchanged - existing scripts work without modification
--jsonis opt-in, not breaking- Exit codes remain consistent (0 = success, 1 = error)
-
Always validate JSON output
result=$(polis --json init) echo "$result" | jq empty || exit 1 # Fail on invalid JSON
-
Capture stderr separately for debugging
result=$(polis --json post test.md 2> error.log) -
Use jq for robust parsing
# Good - handles missing fields gracefully hash=$(echo "$result" | jq -r '.data.content_hash // "unknown"') # Bad - fails on missing field hash=$(echo "$result" | grep -o '"content_hash":"[^"]*"')
-
Check exit codes before parsing
if result=$(polis --json post test.md 2>&1); then # Parse success response else # Parse error response fi