Note: This is the Bash CLI command reference. The Bash CLI is feature-frozen at v0.56.0. The Go CLI has identical commands, ships as a single binary with no external dependencies, and is the recommended choice for new users. For the webapp (local web interface), see WEBAPP-USER-MANUAL.md.
Command-line tool for managing decentralized social content with cryptographic signing and version control.
The Polis CLI enables authors to:
- Create and sign posts and comments using Ed25519 cryptography
- Manage content with git-based version history
- Publish content as static files with frontmatter metadata
- Render markdown to static HTML with customizable templates
- Request blessings from discovery services for authenticated comment discovery
- Automate workflows with JSON mode for scripting and CI/CD integration
The Go CLI is the recommended CLI for new users. It implements all commands listed below, ships as a single binary, and has no external dependencies (no jq, curl, OpenSSH, or pandoc required).
- Primary command is
post(thepublishalias also works) - Same
--jsonflag for machine-readable output - Same directory structure and file formats
- Download pre-built binaries from the releases page, or build from source:
cd cli-go && go build -o polis ./cmd/polis
The command reference below applies to both CLIs — the commands, flags, and behavior are identical.
The sections below are specific to the Bash CLI. If you're using the Go CLI, you only need the single
polisbinary.
- OpenSSH 8.0+ (for Ed25519 signing with
-Yflag) - jq (JSON processor for index management and JSON mode)
- curl (for API communication with discovery service)
- sha256sum or shasum (for content hashing)
- git (optional, for version control integration)
- pandoc (optional, required only for
polis rendercommand)
# Linux (Debian/Ubuntu)
sudo apt-get install openssh-client jq curl coreutils git
sudo apt-get install pandoc # Optional: for polis render
# macOS
brew install openssh jq curl coreutils git
brew install pandoc # Optional: for polis render# Option 1: Add to PATH (quick start)
export PATH="/path/to/polis-cli/cli-bash:$PATH"
# Option 2: Create symlink
sudo ln -s /path/to/polis-cli/cli-bash/polis /usr/local/bin/polis
# Verify installation
polis --helpFor production use, copy the CLI tools directly into your content repository. This keeps your site self-contained—CLI versions are locked to your content, and everything deploys together.
# Clone polis-cli
git clone https://github.com/vdibart/polis-cli.git
# Create your content repository
mkdir my-site && cd my-site
git init
# Copy CLI tools and themes
cp ../polis-cli/cli-bash/polis ./bin/
cp ../polis-cli/cli-bash/polis-upgrade ./bin/
cp -r ../polis-cli/themes ./themes/
# Initialize your site
./bin/polis init
# Add bin/ to .gitignore if you don't want to version the CLI
# Or commit it to lock the CLI version with your contentWhat to copy:
bin/polis— Main CLIbin/polis-upgrade— Upgrade script (optional)themes/— Theme templates (required forpolis initandpolis render)
Why this approach:
- Your site is fully self-contained
- CLI version is locked to your content (no surprise breakage from updates)
- Works offline after initial setup
- Easy to deploy—just push your repo
For a quick introduction, see the README. This guide covers detailed usage.
For a more detailed file structure reference including webapp-specific files, see WEBAPP-USER-MANUAL.md §File Structure.
After running polis init, your directory will contain:
.
├── .polis/
│ ├── keys/
│ │ ├── id_ed25519 # Private signing key (keep secret!)
│ │ └── id_ed25519.pub # Public verification key
│ └── themes/ # Installed themes
│ ├── turbo/ # Retro computing theme
│ │ ├── index.html
│ │ ├── post.html
│ │ ├── comment.html
│ │ ├── comment-inline.html
│ │ ├── turbo.css
│ │ └── snippets/
│ ├── zane/ # Neutral dark theme
│ └── sols/ # Nine Sols inspired theme
├── metadata/ # Metadata files
│ ├── public.jsonl # Content index (JSONL format)
│ ├── blessed-comments.json # Index of approved comments
│ ├── manifest.json # Site metadata (active_theme)
│ └── following.json # Following list
├── posts/ # Your posts
│ └── 20260106/ # Date-stamped directory (YYYYMMDD)
│ ├── .versions/ # Version history for posts in this directory
│ │ └── my-post.md
│ ├── my-post.md
│ └── my-post.html # Generated by polis render
├── comments/ # Your comments
│ └── 20260106/ # Date-stamped directory (YYYYMMDD)
│ ├── .versions/ # Version history for comments in this directory
│ │ └── reply.md
│ ├── reply.md
│ └── reply.html # Generated by polis render
├── snippets/ # Global snippets (override theme snippets)
│ └── about.md # Custom about section
├── styles.css # Active theme's stylesheet (copied on render)
├── index.html # Site index (generated by polis render)
└── .well-known/
└── polis # Public metadata (author, public key, site_title)
Initialize a new Polis directory with keys and metadata.
polis init
polis init --site-title "My Awesome Blog"
polis init --site-title "My Blog" --registerOptions:
--site-title <title>- Set a custom site title for branding (optional)--register- Auto-register with discovery service after init (requiresPOLIS_BASE_URLand discovery service credentials)--posts-dir <dir>- Custom posts directory (default:posts)--comments-dir <dir>- Custom comments directory (default:comments)--keys-dir <dir>- Custom keys directory (default:.polis/keys)--themes-dir <dir>- Custom themes directory (default:.polis/themes)
Creates:
.polis/keys/- Ed25519 keypair for signing.polis/themes/- Installed themes (turbo, zane, sols)posts/,comments/- Content directoriesmetadata/- Metadata directory.well-known/polis- Public metadata filemetadata/public.jsonl- Content index (JSONL format)metadata/blessed-comments.json- Blessed comments indexmetadata/following.json- Following listmetadata/manifest.json- Site metadata (includesactive_themeif set)
Sign and publish a post or comment with frontmatter metadata.
polis post posts/my-post.md
polis post comments/my-comment.mdWhat it does:
- Generates SHA-256 hash of content
- Signs content with Ed25519 private key
- Adds frontmatter with metadata (version, author, signature)
- Appends entry to
public.jsonlindex - Creates
.versionsfile for version history
Example output:
[i] Content hash: sha256:a3b5c7d9...
[i] Signing content with Ed25519 key...
[✓] Created canonical file: posts/20260106/my-post.md
Republish an existing file with updated content (creates new version).
# Edit your published file
vim posts/20260106/my-post.md
# Republish with new version
polis republish posts/20260106/my-post.mdWhat it does:
- Generates diff between old and new content
- Appends diff to
.versionsfile - Updates frontmatter with new version hash
- Re-signs content with new signature
- Rebuilds
public.jsonlindex (prevents duplicates)
Version history:
- Stored in
.versions/subdirectory alongside content files - Example:
posts/20260106/my-post.md→posts/20260106/.versions/my-post.md - Directory name configurable via
POLIS_VERSIONS_DIR_NAME(default:.versions) - Uses unified diff format (compatible with
diff/patchtools) - Enables version reconstruction
Snippets are reusable content fragments for templates. Unlike posts and comments, snippets don't require signing - just place plain .md or .html files in the snippets/ directory.
# Create a snippet - just write a file
echo "Hi, I'm Vincent." > snippets/about.md
# Use nested directories for organization
mkdir -p snippets/homepage
echo "<footer>© 2026</footer>" > snippets/homepage/footer.htmlSnippets vs Themes:
- Snippets are content fragments stored in
snippets/(global) or theme directories - Themes are complete styling packages stored in
.polis/themes/ - Snippets can be included in templates using
{{> path/to/snippet}} - Snippet lookup order: global snippets first, then theme snippets (author overrides theme)
Snippet syntax:
{{> about}}- Default lookup (global → theme), tries .md then .html{{> theme:about}}- Force theme-first lookup{{> global:about}}- Explicit global-first lookup{{> about.md}}or{{> about.html}}- Load specific file extension
File formats:
.mdfiles are processed through pandoc (markdown → HTML).htmlfiles are used as-is
Example snippet (snippets/about.md):
Hi, I'm Vincent. I build things with code and think deeply about
how technology shapes human connection.Note: Snippets don't need frontmatter - just the content. If you have existing snippets with frontmatter from older versions, they'll continue to work (frontmatter is stripped during render).
You can pipe content directly to polis post and polis comment without creating temporary files:
# Basic usage
echo "# My Post" | polis post -
# Specify filename
echo "# My Post" | polis post - --filename my-post.md
# Specify title
echo "Content without heading" | polis post - --title "My Title"
# Both options
echo "Content" | polis post - --filename post.md --title "My Title"
# From file redirect
polis post - < draft.md
# From curl
curl -s https://example.com/draft.md | polis post -
# From heredoc
polis post - << 'EOF'
# My Post
Content here
EOF
# Piping with preprocessing
grep -v "^Draft:" draft.md | polis post - --filename final.mdOptions:
--filename <name>- Specify output filename (default:stdin-TIMESTAMP.md)--title <title>- Override title extraction
For comments:
# Comment from stdin
echo "# My reply" | polis comment - https://bob.com/posts/original.md
# With filename
echo "# Reply" | polis comment - https://bob.com/post.md --filename my-reply.md
# With title override
echo "Content" | polis comment - https://bob.com/post.md --title "My Reply"JSON mode:
echo "# Test" | polis --json post - --filename test.md | jqCreate a comment in reply to a post or another comment (nested threads).
# Reply to a post
polis comment https://alice.example.com/posts/20260106/hello.md
# Reply to another comment (nested thread)
polis comment https://bob.example.com/comments/20260105/reply.md my-reply.md
# From file
polis comment https://bob.example.com/posts/intro.md my-reply.mdWhat it does:
- Creates comment file in
comments/YYYYMMDD/ - Adds
in_reply_tofrontmatter (withurlandroot-postfields) - Signs and publishes comment
- Appends entry to
public.jsonlindex - Automatically requests blessing from discovery service
Nested threads: When replying to a comment (instead of a post), the CLI automatically detects this and fetches the original post URL (root-post) from the discovery service.
Frontmatter structure:
in-reply-to:
url: https://bob.com/comments/reply.md # Immediate parent (post or comment)
root-post: https://alice.com/posts/intro.md # Always the original postPreview content at a URL (posts or comments) with signature verification.
# Preview a post
polis preview https://alice.com/posts/20260105/hello.md
# Preview a comment before blessing it
polis preview https://bob.com/comments/20260105/reply.md
# JSON mode for scripting
polis --json preview https://alice.com/posts/hello.mdWhat it shows:
- Frontmatter metadata (displayed dimmed/greyed)
- Signature verification status (valid/invalid/missing)
- Content hash verification (valid/mismatch)
- For comments: the in-reply-to URL
- The content body
Use cases:
- Preview a comment before blessing it
- Preview content before responding to it
- Verify content integrity and authenticity
JSON mode: See JSON-MODE.md for response format.
Rebuild local indexes and reset state. Automatically regenerates manifest.json after any rebuild.
# Rebuild posts/comments index
polis rebuild --posts
# Rebuild blessed comments index
polis rebuild --comments
# Reset notification files
polis rebuild --notifications
# Rebuild everything
polis rebuild --allFlags (combinable):
--posts- Rebuildpublic.jsonlfrom posts and comments on disk--comments- Rebuildblessed-comments.jsonfrom discovery service--notifications- Reset notification files (.polis/notifications.jsonl,.polis/notifications-manifest.json)--all- All of the above
Note: manifest.json is automatically regenerated after any rebuild operation.
Use when:
- Index is corrupted or out of sync
- You manually edited published files
- You restored from backup
- Notification state is corrupted
- CLI version was upgraded (clears "metadata files need update" warning)
View the content index in JSONL or JSON format (read-only, outputs to stdout).
# View as JSONL (default)
polis index
# View as JSON (grouped by type)
polis index --json
# Pipe to jq for pretty printing
polis index --json | jq
# Count posts
polis index | grep -c '"type":"post"'
# View recent 10 entries
polis index | tail -10 | jq
# Extract all post titles
polis index --json | jq -r '.posts[].title'What it does:
- Reads
metadata/public.jsonl - Default: outputs raw JSONL (one entry per line)
--jsonflag: converts to grouped JSON format for readability- Read-only operation - never modifies the index file
Use cases:
- Debugging index contents
- Scripting and automation
- Inspecting published content metadata
Reconstruct a specific version of a file from version history.
# Get specific version
polis extract posts/20260106/my-post.md sha256:abc123...
# Output to file
polis extract posts/20260106/my-post.md sha256:abc123... > old-version.mdTo completely reset your Polis installation and start over, move or remove the following files/directories, then run polis init:
.polis/- Configuration and signing keys.well-known/- Public metadataposts/- Published postscomments/- Published commentsmetadata/- Index and blessing data
Example:
rm -rf .polis .well-known posts comments metadata
polis initNote: This will generate new signing keys. If you want to keep your identity, back up .polis/keys/ before removing.
Migrate all content to a new domain. This command handles the complete migration process including re-signing files and updating the discovery service database.
polis migrate newdomain.comWhat it does:
- Auto-detects current domain from published files
- Updates
canonical_urlin all posts and comments - Updates
in_reply_toandroot_postURLs (only for own content) - Re-signs all files with new URLs
- Updates
metadata/blessed-comments.json - Updates
.well-known/polisendpoints - Rebuilds
metadata/public.jsonlindex - Updates discovery service database (preserves blessing status)
- Stages all changes in git
Example output:
[✓] Migration complete!
Old domain: olddomain.com → New domain: newdomain.com
Posts: 3, Comments: 5, Database rows: 5
JSON mode: See JSON-MODE.md for response format.
Important notes:
- Domain should not include protocol (use
example.com, nothttps://example.com) - Comments you made on others' posts will have their
in_reply_to/root_postpreserved (pointing to the other author's domain) - The discovery service database update requires
DISCOVERY_SERVICE_URLandDISCOVERY_SERVICE_KEYto be configured - If database update fails, local migration still succeeds - you can re-beseech comments later
Print the CLI version number.
polis versionExample output:
polis 0.22.0
Display complete system information including site details, versions, configuration, keys, discovery status, and project links.
polis about
polis --json aboutExample output:
Polis - Decentralized Social Networking
────────────────────────────────────────
SITE https://example.com (My Blog)
CLI 0.29.0
KEYS initialized (SHA256:abc123...)
DISCOVERY registered
Displays site details, versions, configuration paths, key status, and discovery registration.
Registration status values:
- registered - Site is registered with the discovery service
- not registered - Site is not publicly listed (run
polis registerto join the directory) - discovery not configured -
DISCOVERY_SERVICE_URLorDISCOVERY_SERVICE_KEYnot set - check failed - Could not reach the discovery service
JSON mode: Returns structured data with all sections. See JSON-MODE.md for the full JSON response format.
List your site in the public directory. Registration makes your site discoverable to other authors and allows you to participate in conversations across the polis network.
polis registerFeatures:
- Idempotent - Running on an already-registered site shows current status
- Attestation verification - Verifies the discovery service's signature on your registration
- Automatic metadata - Pulls email and author name from
.well-known/polisif available
Example output (new registration):
[✓] Site registered successfully!
Registration Details:
Domain: example.com
Registry: https://ds.polis.pub
Registered: 2026-01-18T12:00:00Z
Your site is now listed in the public directory.
Other authors can now discover your content and engage with your posts.
Example output (already registered):
[✓] Site already registered.
Registration Details:
Domain: example.com
Registry: https://ds.polis.pub
Registered: 2026-01-15T10:30:00Z
[✓] Attestation Verification: Valid
(Server signature verified against locally reconstructed payload)
Requirements:
DISCOVERY_SERVICE_URLandDISCOVERY_SERVICE_KEYmust be setPOLIS_BASE_URLmust be set (domain is extracted from this)- Your
.well-known/polismust be accessible via HTTPS
JSON mode: Returns registration details including service_attestation for verification.
Unregister your site from the discovery service. This performs a hard delete (privacy promise) - all registration data is permanently removed.
# Interactive confirmation required
polis unregister
# Skip confirmation (for scripting)
polis unregister --forceWarning displayed:
WARNING: Unregistering will remove your site from the public directory.
This means:
- Your site will no longer be publicly listed or discoverable
- Other authors cannot find or interact with your content through the network
- You can re-register anytime to rejoin the community
Are you sure you want to unregister example.com? (type 'yes' to confirm)
Effects of unregistering:
- Your site is removed from the public directory
- Your content is no longer discoverable through the network
- Other authors cannot interact with your posts via the discovery service
- You can rejoin anytime with
polis register
JSON mode: Skips interactive confirmation automatically.
Render markdown posts and comments to static HTML files using pandoc.
# Render all posts and comments (skips up-to-date files)
polis render
# Force re-render all files
polis render --force
# Render without snippet markers (for production/clean HTML)
polis render --no-markersWhat it does:
- On first render, automatically selects a theme from available themes
- Converts all markdown files in
posts/andcomments/to HTML - Uses pandoc for markdown-to-HTML conversion
- Applies theme templates with metadata substitution
- Embeds blessed comments directly in post HTML files
- Copies theme CSS to
styles.cssat site root - Generates an
index.htmllisting all posts - Skips files where HTML is newer than markdown (unless
--force) - Note: Remote blessed comments are cached. If a comment author updates their comment, use
--forceto fetch the latest content.
Requires: pandoc (install with apt install pandoc or brew install pandoc)
Generated files:
posts/YYYYMMDD/my-post.html- Rendered post with embedded blessed commentscomments/YYYYMMDD/my-comment.html- Rendered commentindex.html- Site index listing all posts
Example output:
=== Render Configuration ===
[i] POLIS_BASE_URL: https://example.com
[i] Posts dir: posts
[i] Comments dir: comments
[i] Blessed comments: 2 post(s) with blessed comments
=== Rendering Posts ===
Processing: posts/20260106/hello.md
-> Rendered: posts/20260106/hello.html
=== Rendering Comments ===
Processing: comments/20260106/reply.md
-> Rendered: comments/20260106/reply.html
=== Generating Index ===
Generated: index.html
[✓] Rendering complete!
[i] Posts rendered: 1 (skipped: 0)
[i] Comments rendered: 1 (skipped: 0)
[i] Index: index.html
Polis ships with six themes (turbo, zane, sols, vice, especial, especial-light). On first render, a theme is randomly selected. To change themes:
- Dashboard: Open Settings > Theme and click a theme card. The site re-renders automatically.
- CLI: Edit
metadata/manifest.json, setactive_theme, then runpolis render --force.
For theme customization, creating custom themes, template variables, and mustache syntax, see TEMPLATING.md.
Each rendered HTML file includes the original markdown source and frontmatter in an HTML comment at the end of the file:
<!--
=== POLIS SOURCE ===
Source: posts/20260106/hello.md
title: Hello World
published: 2026-01-06T12:00:00Z
current-version: abc123...
signature: AAAAB3NzaC1...
---
This is my first post!
=== END POLIS SOURCE ===
-->This enables:
- Verification that the HTML matches the signed source
- Extraction of original markdown from rendered HTML
- Debugging template issues by comparing source to output
By default, polis render injects hidden markers around snippet content to enable in-browser snippet editing in the webapp. Each snippet inclusion ({{> snippet-name}}) is wrapped with:
<!-- POLIS-SNIPPET-START: global:snippet-name path=snippets/snippet-name.html -->
<span class="polis-snippet-boundary" data-snippet="global:snippet-name"
data-path="snippets/snippet-name.html" data-source="global" hidden></span>
<!-- actual snippet content -->
<!-- POLIS-SNIPPET-END: global:snippet-name -->Marker behavior:
- The hidden
<span>provides a DOM anchor for JavaScript without affecting layout data-snippetcontains the snippet identifier (source:name format)data-pathcontains the file path for API callsdata-sourceindicates "global" (fromsnippets/) or "theme" (from theme)- Nested snippets each get their own markers, creating a hierarchy
Disabling markers:
For production deployments or when you want clean HTML without markers:
polis render --no-markersThis is useful for:
- Production builds where markers are unnecessary
- Debugging template output without extra HTML
- Compatibility with strict HTML validators
JSON mode: See JSON-MODE.md for response format.
Parent command for blessing-related operations. Must be followed by a subcommand.
List pending blessing requests for your posts.
polis blessing requestsExample output:
ID Author Post Status
42 bob@example.com /posts/hello.md pending
73 carol@example.com /posts/hello.md pending
Approve a pending blessing request by content hash.
polis blessing grant abc123-def456What it does:
- Updates discovery service status to "blessed"
- Adds entry to
metadata/blessed-comments.json - Comment becomes visible to your audience
Reject a pending blessing request by content hash.
polis blessing deny abc123-def456What it does:
- Updates discovery service status to "denied"
- Comment remains on author's site but won't be amplified
Re-request blessing for a comment by content hash (retry after changes).
polis blessing beseech abc123-def456Use when:
- Original request failed
- You updated the comment and want to re-request
Synchronize auto-blessed comments from the discovery service to your local blessed-comments.json.
polis blessing syncWhat it does:
- Fetches all blessed comments for your posts from discovery service
- Compares with local
metadata/blessed-comments.json - Adds any missing entries (e.g., comments auto-blessed while you were offline)
When to use:
- After being offline for a while
- To ensure local file matches discovery service
- Automatically called when running
polis blessing requests
Example output:
[i] Syncing blessed comments from discovery service...
[✓] Synced 3 comment(s) to blessed-comments.json
Follow an author to auto-bless their future comments on your posts.
polis follow https://alice.example.comWhat it does:
- Adds author to
metadata/following.json - Auto-blesses any existing pending comments from this author
- Future comments from this author are automatically blessed
Example output:
[OK] Following alice.example.com
[OK] 3 existing comments auto-blessed
Stop following an author and hide all their comments.
polis unfollow https://alice.example.comWhat it does:
- Removes author from
metadata/following.json - Removes all blessed comments from this author (nuclear option)
Warning: This is a destructive action - all previously blessed comments from this author will be hidden.
View and manage notifications about activity on your site and from authors you follow.
# List unread notifications (default)
polis notifications
# List all notifications
polis notifications list
# Show specific types
polis notifications list --type new_follower,version_available
# JSON output
polis --json notificationsNotification types:
version_available- A new CLI version is availableversion_pending- You upgraded but metadata files need update (runpolis rebuild)new_follower- Someone you don't follow started following younew_post- An author you follow published a new postblessing_changed- Your comment was blessed or unblessed
Mark a notification as read (removes it from the list).
polis notifications read notif_1737388800_abc123
# Mark all as read
polis notifications read --allDismiss a notification without marking as read.
polis notifications dismiss notif_1737388800_abc123
# Dismiss old notifications
polis notifications dismiss --older-than 30dSync notifications from the discovery service.
# Fetch new notifications
polis notifications sync
# Reset watermark and do full re-sync
polis notifications sync --resetConfigure notification preferences.
# Show current config
polis notifications config
# Set poll interval
polis notifications config --poll-interval 30m
# Enable/disable notification types
polis notifications config --enable new_post
polis notifications config --disable version_available
# Mute notifications from specific domain
polis notifications config --mute spam.com
polis notifications config --unmute spam.comLocal storage:
.polis/notifications.jsonl- Notification log (one per line).polis/notifications-manifest.json- Preferences and sync state
When following or unfollowing an author, you can optionally announce this to the discovery service:
# Follow and announce (others can discover you follow alice)
polis follow https://alice.com --announce
# Unfollow and announce
polis unfollow https://alice.com --announcePrivacy note: Without --announce, follow/unfollow is local-only. With --announce, your follow action is recorded in the discovery service (opt-in).
Comments can be automatically blessed (no manual approval required) in two scenarios:
1. Global Trust (Following) When you follow an author, ALL their future comments on ANY of your posts are auto-blessed.
# Follow Alice - all her comments on your posts are now auto-blessed
polis follow https://alice.example.com2. Thread-Specific Trust When you manually bless a comment from an author, their future comments on the same post are auto-blessed. This allows trust to be scoped to specific conversations.
Example:
1. Bob comments on your "Intro to Polis" post
2. You bless Bob's comment (polis blessing grant 123)
3. Bob comments again on "Intro to Polis" → auto-blessed!
4. Bob comments on your "Advanced Polis" post → NOT auto-blessed (different post)
Precedence: Global trust (following) takes priority. If you follow someone, thread-specific trust is irrelevant - they're trusted everywhere.
Published files include YAML frontmatter with metadata:
---
canonical_url: https://alice.example.com/posts/20260106/hello.md
version: sha256:a3b5c7d9e1f2...
author: alice@example.com
published: 2026-01-15T12:00:00Z
signature: -----BEGIN SSH SIGNATURE-----
U1NIU0lHAAAAAQA...
-----END SSH SIGNATURE-----
in_reply_to: https://bob.example.com/posts/intro.md # Comments only
in_reply_to_version: sha256:xyz789... # Comments only
---
# Your Content Here
The actual post or comment content follows the frontmatter.Polis uses diff-based version storage. The .versions file format uses standard unified diff format, making it compatible with Unix diff and patch utilities for manual inspection or reconstruction.
Example .versions file:
== Version 1 ==
Version: sha256:abc123...
Date: 2026-01-15T12:00:00Z
Full content of version 1...
== Version 2 ==
Version: sha256:def456...
Date: 2026-01-20T15:30:00Z
Previous: sha256:abc123...
--- old
+++ new
@@ -5,7 +5,7 @@
-This is the old line
+This is the updated line
Polis CLI uses a layered configuration system with the following precedence (highest to lowest):
- Environment variables - For CI/CD and temporary overrides
.envfile - For developer/deployment settings.well-known/polis- For user-specific directory customization- Built-in defaults - Always available as fallback
# Required for blessing commands
export POLIS_BASE_URL="https://yourdomain.com"
# Discovery service (optional - has default)
export DISCOVERY_SERVICE_URL="https://ds.polis.pub"
# API authentication (required for blessing operations)
export DISCOVERY_SERVICE_KEY="your-api-key"
# Optional directory overrides
export KEYS_DIR=".polis/keys"
export POSTS_DIR="posts"
export COMMENTS_DIR="comments"
export VERSIONS_DIR_NAME=".versions"Add to ~/.bashrc or ~/.zshrc for persistence.
The CLI looks for .env in this order:
- Current working directory (
.env) - for per-site configuration - Home directory (
~/.polis/.env) - for shared configuration across sites
Create a .env file in your site directory or in ~/.polis/:
# Per-site config (in your polis site directory)
cp .env.example .env
# Or shared config (for DISCOVERY_SERVICE_KEY etc.)
mkdir -p ~/.polis
cp .env.example ~/.polis/.envExample .env:
POLIS_BASE_URL=https://alice.example.com
DISCOVERY_SERVICE_KEY=eyJhbGciOiJI...Security Note: Never commit .env files containing secrets. The .env.example file is safe to commit as a template.
Set a custom site title for branding in rendered HTML and comment attribution:
polis init --site-title "My Awesome Blog"The site title is stored in .well-known/polis and used:
- In HTML page titles and headers (
{{site_title}}template variable) - When displaying your comments on other people's posts
- In
polis aboutoutput
If not set, the domain from POLIS_BASE_URL is used as a fallback.
You can customize directory paths during initialization:
polis init --posts-dir articles --comments-dir replies
polis init --site-title "My Blog" --posts-dir articlesOr edit the config section in .well-known/polis after initialization:
{
"version": "0.2.0",
"config": {
"directories": {
"keys": ".polis/keys",
"posts": "articles",
"comments": "replies",
"versions": ".versions"
},
"files": {
"public_index": "metadata/public.jsonl",
"blessed_comments": "metadata/blessed-comments.json",
"following_index": "metadata/following.json"
}
}
}All commands support --json for machine-readable output:
polis --json post my-post.md | jq -r '.data.content_hash'See JSON-MODE.md for response schemas, error codes, and scripting examples.
vim posts/my-thoughts.mdpolis post posts/my-thoughts.mdgit add .
git commit -m "Add: my-thoughts.md"git push origin main# Note: polis comment and polis republish automatically request blessings
# You rarely need to run this manually - only for edge cases (see docs)
# If you do need to retry a blessing request:
polis blessing requests # View pending requests
polis blessing beseech <hash> # Retry request by hashcat > posts/why-decentralization-matters.md << 'EOF'
# Why Decentralization Matters
Centralized platforms have too much control...
EOF
polis post posts/why-decentralization-matters.md
git add . && git commit -m "New post: decentralization"
git pushpolis comment https://alice.com/posts/20260106/hot-take.md
# Interactive editor opens - write your reply
# After saving, blessing request is automatically sent
# Your comment will be pending until the post author blesses it# Edit the canonical file directly
vim posts/20260106/my-post.md
# Republish with new version
polis republish posts/20260106/my-post.md
git add . && git commit -m "Update: my-post.md"
git push# List all versions in .versions file
cat posts/20260106/.versions/my-post.md
# Reconstruct specific version
polis extract posts/20260106/my-post.md sha256:abc123...- Never commit
.polis/keys/id_ed25519(private key) - Add to
.gitignore:.polis/keys/id_ed25519 - Public key (
.polis/keys/id_ed25519.pub) is safe to share
Anyone can verify your content signatures:
# Extract public key from .well-known/polis
curl https://alice.com/.well-known/polis | jq -r .public_key > alice.pub
# Verify signature (manual process - automated tool coming)
ssh-keygen -Y verify -f alice.pub -I alice@example.com -n file \
-s signature.sig < content.txtEach published file (.md) - both posts and comments - contains two integrity fields in its frontmatter:
current-version (Content Hash)
The SHA-256 hash of the entire file content (frontmatter + body), canonicalized:
current-version: sha256:a1b2c3d4e5f6...
Canonicalization ensures consistent hashing:
- Trailing whitespace removed from each line
- Trailing empty lines removed
- Exactly one newline at the end
signature (Cryptographic Signature)
The Ed25519 signature is computed over the entire file content minus the signature field itself, canonicalized:
signature: AAAAB3NzaC1lZDI1NTE5...
This means the signature covers:
- All frontmatter fields (title, published, type, current-version, in-reply-to, etc.)
- The entire body content
If you manually edit a published post or comment and deploy without running polis republish:
| Change Made | Hash Valid? | Signature Valid? | Consequence |
|---|---|---|---|
| Edit body text | ❌ No | ❌ No | Verification fails |
| Edit title | ❌ No | ❌ No | Verification fails |
| Edit published date | ❌ No | ❌ No | Verification fails |
| Edit current-version | ❌ No | ❌ No | Verification fails |
| Edit any frontmatter | ❌ No | ❌ No | Verification fails |
| Trailing whitespace only | ✅ Maybe* | ✅ Maybe* | May pass due to canonicalization |
*Canonicalization normalizes trailing whitespace, so minor whitespace changes may not break verification.
Practical consequences of editing without republishing:
polis preview <url>shows: "Signature: FAILED"- Other polis users see the content as tampered/unverified
- New blessing requests may fail verification
- Version history becomes inconsistent (hash doesn't match content)
Safe fields to change: None. Any edit to a published .md file requires polis republish to:
- Recompute the content hash
- Re-sign with your private key
- Update the version history
Files you CAN safely edit:
- Rendered
.htmlfiles (not signed, regenerated bypolis render) - Theme/template files
- Configuration files
The canonical URL of your content is not stored in the file itself - it's derived from POLIS_BASE_URL + file_path. This means:
What's in the frontmatter:
# Posts - NO URL field
title: My Post
published: 2026-01-26T12:00:00Z
current-version: sha256:...
signature: ...
# Comments - in-reply-to points to PARENT's URL, not yours
in-reply-to:
url: https://other-person.com/posts/their-post.md
root-post: https://other-person.com/posts/their-post.mdIf you change POLIS_BASE_URL without running polis migrate:
| What | Status | Why |
|---|---|---|
| Your signatures | ✅ Valid | URL not in signed content |
| Your content hashes | ✅ Valid | URL not in file content |
| Discovery service records | ❌ Broken | Still indexed by old URLs |
| Others' links to your posts | ❌ Broken | Point to old domain |
Others' in-reply-to fields |
❌ Broken | Their comments reference your old URLs |
Your blessed-comments.json |
❌ Broken | Post URLs use old domain |
Bottom line: Cryptographically valid, but operationally broken.
Why polis migrate exists:
- Updates discovery service URL indexes
- Creates signed migration announcement for others to verify
- Proves key continuity (same private key controls both domains)
- Allows followers to update their local references
Always use polis migrate <new-domain> when changing domains - don't just edit POLIS_BASE_URL.
Deprecated — The TUI is deprecated as of v0.46.0. Use the webapp instead for an interactive interface.
For version migrations and binary updates, see UPGRADING.md.
Polis includes tab completion scripts for bash and zsh. After setup, you can type polis i<tab> to complete to init.
Add to your ~/.bashrc:
source /path/to/polis/completions/polis.bashOr for auto-loading, copy to the bash-completion directory:
cp completions/polis.bash ~/.local/share/bash-completion/completions/polisAdd the completions directory to your fpath in ~/.zshrc:
fpath=(/path/to/polis/completions $fpath)
autoload -Uz compinit && compinitOr copy to your zsh completions directory:
mkdir -p ~/.zsh/completions
cp completions/polis.zsh ~/.zsh/completions/_polisThen add to ~/.zshrc:
fpath=(~/.zsh/completions $fpath)
autoload -Uz compinit && compinit- All 24 polis commands (init, post, comment, etc.)
- Subcommands for
blessingandmigrations - Global
--jsonflag
Install OpenSSH client (see Installation section above).
Install jq JSON processor (see Installation section above).
Install pandoc to use polis render: apt install pandoc (Linux) or brew install pandoc (macOS). Pandoc is only required for the render command.
Run polis init to create keys and directory structure.
Run polis rebuild to regenerate public.jsonl from published files.
.versions files are created on first polis republish - they don't exist for initial polis post.
- Deploy your content to GitHub Pages, Netlify, or any static host
- Read SECURITY-MODEL.md for the full cryptographic model and threat analysis
- Customize your site with TEMPLATING.md
- Try the webapp for a visual interface
For issues, questions, or feature requests, please file an issue in the GitHub repository.
AGPL-3.0 - See LICENSE