Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
Alessandro Vigano avatar

Margins

  • Updated July 27, 2026
  • alvistar/margins-cli

margins is a Claude Code skill in the Code Review & Quality category. Margins code review platform: post discussions, read feedback, manage workspaces from Claude Code

Key points

  • margins
  • Code Review & Quality
  • AI-coding skill

Margins by the numbers

  • Data as of Jul 28, 2026 (Skillselion catalog sync)
/plugin marketplace add alvistar/margins-cli
/plugin install margins@margins-cli

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Last updatedJuly 27, 2026
Repositoryalvistar/margins-cli

What it does

Margins code review platform: post discussions, read feedback, manage workspaces from Claude Code

README.md

margins-cli

CLI for Margins — review layer for Markdown in Git.

Margins is a review platform where humans and AI agents are equal participants. It renders Markdown files from a Git repository in a clean UI where reviewers can open discussions, propose changes, and approve content. margins-cli exposes every Margins action as a shell command, making it usable in scripts, CI pipelines, and by AI agents.

Installation

Install globally (recommended):

npm install -g margins-cli

Run without installing:

npx margins-cli <command>

Or clone and build locally:

git clone https://github.com/alvistar/margins-cli.git
cd margins-cli
npm install && npm run build
npm link   # makes 'margins' available globally

Claude Code plugin

The Claude Code plugin that wraps this CLI moved to the margins-plugins marketplace repo. Add that marketplace and install the margins plugin there; this repo is the npm CLI only.

Quick Start

# Log in via browser (one-time)
margins auth login

# See your workspaces
margins workspace list

# Push local markdown to a brand-new workspace (no GitHub repo needed)
margins workspace push --project my-docs --dir ./docs

# Wire `git push` to auto-sync this repo to Margins (non-blocking)
margins install-hook

# Onboard a repo to credentialless CI sync (workspace + OIDC binding + workflow PR)
margins install

# List open discussions in the current repo
margins discuss list

# Create a discussion on a file
margins discuss create --path docs/intro.md --body "This section needs a concrete example."

Authentication

Three methods are supported. The active credential is resolved in priority order:

Priority Source Set by
1 GitHub Actions OIDC token MARGINS_OIDC_TOKEN, or minted in CI from ACTIONS_ID_TOKEN_REQUEST_*
2 --api-key <key> flag any command
3 MARGINS_API_KEY env var shell / CI environment
4 Stored static API key margins config set-key
5 Stored Keycloak access token margins auth login

Browser login (recommended for humans)

margins auth login

Opens your browser to complete OAuth 2 PKCE against Keycloak. On success, the access token and refresh token are stored locally. The token is refreshed automatically before it expires — no re-login needed.

Static API key (recommended for CI / agents)

Mint a key from the Margins web UI or the API (POST /api/keys), then store it:

margins config set-key mrgn_...
# or per-invocation:
margins --api-key mrgn_... workspace list
# or via environment:
MARGINS_API_KEY=mrgn_... margins workspace list

Static keys support two scopes: comment (read + create discussions) and edit (full write access).

GitHub Actions OIDC (credentialless CI)

In GitHub Actions, workspace push authenticates with a short-lived, GitHub-signed OIDC token — no API key stored in the repo or anywhere else. Either set MARGINS_OIDC_TOKEN to a pre-minted token, or grant the workflow permissions: id-token: write and the CLI mints one itself from ACTIONS_ID_TOKEN_REQUEST_URL / ACTIONS_ID_TOKEN_REQUEST_TOKEN (re-minting automatically on a mid-push 401, since large first syncs can outlive a token's ~5-minute life). The server verifies the token against GitHub's JWKS and authorizes by a pre-registered trust binding written via margins install.

The easiest way to wire this up is the margins-sync-action composite action, which margins install stamps into the repo for you. Requires Margins server v0.21.0+.


Commands

Global Flags

Available on every command.

Flag Description
-v, --version Print version and exit
--json Output as JSON — for scripting and agents
--verbose Enable debug logging
--no-color Disable ANSI colors
--server-url <url> Override the server URL (default: https://margins.app)
--api-key <key> Override the API key for this invocation

config

Manage local CLI configuration.

config show

Display the active configuration.

margins config show
margins config show --json

Shows the active server URL, the masked API key or token, and whether auth came from auth login or config set-key.

config set-key <key>

Store a static Margins API key.

margins config set-key mrgn_abc123...

Saves the key to the global config file. Clears any previously stored Keycloak session.

config set-url <url>

Override the server URL (useful for self-hosted Margins instances).

margins config set-url https://margins.example.com

auth

Authentication commands.

auth login

Log in via browser using Keycloak OAuth 2 + PKCE.

margins auth login

Opens a browser window to complete the OAuth flow. On completion, stores the Keycloak access token and refresh token locally. Subsequent commands use the token automatically, refreshing it transparently when it expires.

Note: The Keycloak client must have http://localhost:* registered as a valid redirect URI. See TODOS.md for the Keycloak admin configuration step.

auth whoami

Show the currently authenticated identity.

margins auth whoami
margins auth whoami --json

Calls GET /api/auth/whoami and displays your user ID, email, and role.

auth logout

Revoke the stored session and clear local credentials.

margins auth logout

Revokes the Keycloak refresh token and clears the stored access/refresh tokens from the config file. The server URL is preserved.


workspace

Manage Margins workspaces. A workspace is the unit of review in Margins. There are two kinds:

  • GitHub workspaces — connect a GitHub repository. Margins clones the repo and syncs markdown files on demand. Created with workspace create <repo-url>.
  • Local workspaces — no repository. You push markdown files directly via workspace push --project <name>. Useful for solo work, drafts, or content that does not live in a Git repo yet.

You can also push local markdown into a GitHub workspace via workspace push --workspace <id> — the content lands on a virtual @local branch alongside the real git branches, so you can review uncommitted edits before pushing them upstream.

workspace list

List all workspaces you have access to.

margins workspace list
margins workspace list --json

Displays workspace slug, name, sync status, and last synced time.

workspace create <repo-url>

Create a new workspace from a GitHub repository URL.

margins workspace create https://github.com/org/repo

If a workspace for that repository already exists and you are not a member, you will be auto-joined to it.

workspace open [slug]

Open a workspace in the browser.

margins workspace open           # uses slug from .margins.json
margins workspace open my-repo

If no slug is provided, reads workspace_slug from .margins.json in the current directory (or any parent).

workspace sync [slug]

Trigger a git sync to pull the latest content from the repository.

margins workspace sync                      # uses .margins.json
margins workspace sync my-repo
margins workspace sync my-repo --branch main
Flag Description
--branch <branch> Branch to sync (defaults to the workspace's default branch)

Local workspaces cannot be synced this way — they receive content via workspace push. Calling sync on a local workspace returns LOCAL_SYNC_NOT_SUPPORTED (HTTP 422).

workspace push

Push local markdown files to a workspace for review. This is the only way to get content into a local workspace, and the way to overlay uncommitted edits onto a GitHub workspace via the virtual @local branch.

# Create a brand-new local workspace and push files in one step
margins workspace push --project my-docs --dir ./docs

# Push more files to the same workspace later (re-use the workspace ID)
margins workspace push --workspace 0cfbdc14-c023-4c84-bc4a-e027e13cefab --dir ./docs

# Overlay local edits onto an existing GitHub workspace (lands on @local branch)
margins workspace push --workspace <github-workspace-id> --dir ./docs
Flag Required Description
--project <name> one of Create a new local workspace with this name. Slug becomes local/<your-username>/<name>. The name must be alphanumeric (hyphens, dots, underscores allowed).
--workspace <id> one of Push to an existing workspace by UUID. Use this for re-pushes and for pushing into GitHub workspaces.
--dir <path> no Directory to recursively scan for .md files. Defaults to the current directory. Hidden files, node_modules/, and symlinks are skipped.

Behavior:

  • Recursively scans --dir for .md files (max 50 per push, max 1 MB per file, max 10 MB total)
  • For each file, computes a SHA-256 hash of the content. If the hash matches an existing artifact, the file is skipped. Otherwise it is added or changed.
  • Output (with --json):
    { "added": 2, "changed": 0, "skipped": 0 }
    
  • For local workspaces, content lands on the main branch.
  • For GitHub workspaces, content lands on the virtual @local branch — visible in the branch switcher alongside real git branches, but never pushed upstream.

Example: review your local edits before committing them

cd ~/my-project                               # has docs/spec.md, README.md
margins workspace push --workspace <gh-ws-id> # uploads to @local
margins workspace open                         # opens browser, switch to @local branch
# ...review, comment, refine...
git commit -am "Refine spec"                  # then commit for real
margins workspace sync                         # pull the committed version into main

stash

Publish a single markdown document to a Margins stash — a one-off, single-doc workspace for review. No repo, no workspace to pick, no folder binding. Returns a review URL.

margins stash notes.md                      # publish a file
cat notes.md | margins stash                # or pipe markdown via stdin
margins stash notes.md --title "Q3 plan"    # set the title explicitly
margins stash notes.md --json               # machine-readable: { id, slug, url }
Flag Description
--title <title> Title for the stash doc. Defaults to the document's first # heading, then the file name, then "Untitled stash doc".

The document comes from the [file] argument, or from piped stdin when no file is given (or the argument is -). The stash is reviewed like any Margins doc — open discussions, address them, resolve. Markdown only; for images, use workspace push or the Margins desktop app.


discuss

Manage discussions on Markdown artifacts.

discuss list [slug]

List discussions in a workspace.

margins discuss list                     # uses .margins.json, shows open discussions
margins discuss list my-repo
margins discuss list my-repo --status resolved
margins discuss list my-repo --path docs/intro.md
margins discuss list --json
Flag Description Default
--path <path> Filter by artifact path
--status <status> Filter by status: open or resolved open
discuss create [slug]

Create a new discussion on an artifact.

margins discuss create \
  --path docs/intro.md \
  --body "This section needs a concrete example."

margins discuss create my-repo \
  --path docs/api.md \
  --body "Consider adding a rate limit note here." \
  --anchor-heading "Authentication"

margins discuss create my-repo \
  --path docs/api.md \
  --body "Typo: 'recieve' should be 'receive'." \
  --anchor-text "recieve the response"
Flag Required Description
--path <path> yes Artifact path within the workspace
--body <body> yes Discussion body text
--anchor-heading <heading> no Anchor the discussion to a heading
--anchor-text <text> no Anchor the discussion to a text selection
discuss reply <discussion-id>

Post a reply to an existing discussion.

margins discuss reply d_abc123 --body "Fixed in the latest commit."
margins discuss reply d_abc123 --body "Agreed." --workspace my-repo
Flag Required Description
--body <body> yes Reply body text
--workspace <slug> no Workspace slug (alternative to .margins.json)
discuss resolve <discussion-id>

Mark a discussion as resolved.

margins discuss resolve d_abc123
margins discuss resolve d_abc123 --summary "Updated the docs to include this example."
margins discuss resolve d_abc123 --workspace my-repo
Flag Required Description
--summary <summary> no Short description of how the issue was resolved
--workspace <slug> no Workspace slug (alternative to .margins.json)

completions

Generate shell completion scripts.

margins completions -s zsh   # zsh
margins completions -s bash  # bash
margins completions -s fish  # fish
Flag Required Description
-s, --shell <shell> yes Target shell: bash, zsh, or fish
Install

Zsh — add to ~/.zshrc:

eval "$(margins completions -s zsh)"

Bash — add to ~/.bashrc or ~/.bash_profile:

eval "$(margins completions -s bash)"

Fish — add to ~/.config/fish/config.fish:

margins completions -s fish | source

After reloading your shell, press Tab after margins workspace sync to get live workspace slug completion from the API.

install-hook

Installs a git hook that triggers margins workspace push on every push (or commit). The hook is non-blocking: sync runs in the background and git push always succeeds regardless of sync outcome. CLI logs a warning on failure.

margins install-hook              # pre-push hook (default)
margins install-hook --on commit  # post-commit hook
margins install-hook --force      # overwrite an existing hook without prompting
Flag Required Description
--on <trigger> no push (default) or commit. push runs the sync when you git push; commit runs it on every commit.
--force no Overwrite an existing hook file without prompting.

Prerequisite — workspace identification. The hook script calls margins workspace push with no arguments, which reads workspace_id from .margins.json in the repo root. Before installing the hook, register the workspace once:

margins workspace push --workspace <workspace-id>
# or, for a brand-new local workspace:
margins workspace push --project my-docs

After the first push, .margins.json is written and the hook works on subsequent git push. If .margins.json is missing when you run install-hook, the CLI warns you — the hook will fail silently on every push until you create it.

Generated hook (pre-push):

#!/bin/sh
# Margins CAS sync — non-blocking pre-push hook
# Installed by: margins install-hook
margins workspace push &
exit 0

Removing the hook: delete .git/hooks/pre-push (or post-commit) by hand. There is no uninstall-hook command yet.


install

Onboards a repository to credentialless CI sync: creates (or reuses) the Margins workspace, writes the OIDC trust binding that authorizes this repo's GitHub Actions to push, and opens a PR adding the sync workflow. Once it lands, CI pushes markdown on every change with no stored credentials (see Authentication).

margins install                      # onboard the current repo
margins install owner/repo           # onboard a specific repo
margins install --org my-org         # onboard every repo in an org (or user account)
margins install --org my-org --include 'docs-*' --exclude 'archived-*'
margins install --dry-run            # print intended actions without writing anything
Flag Required Description
--org <org> no Install across all repos in a GitHub org or user account.
--include <glob...> no With --org: only repos matching these globs.
--exclude <glob...> no With --org: skip repos matching these globs.
--dry-run no Print the planned workspace / binding / PR actions without writing anything.

The stamped workflow uses the margins-sync-action and a pinned CLI version. Requires Margins server v0.21.0+.


audit

Reports sync coverage across one or many repositories: which are missing the sync workflow, which carry a stale action pin, which have binding drift (the recorded workspace binding no longer matches the repo), and which exceed the server's file cap.

margins audit                        # audit the current repo
margins audit --org my-org           # audit every repo in an org
margins audit --org my-org --csv     # CSV output for spreadsheets
Flag Required Description
--org <org> no Audit all repos in a GitHub org or user account.
--include <glob...> no With --org: only repos matching these globs.
--exclude <glob...> no With --org: skip repos matching these globs.
--csv no Emit CSV instead of a table.

audit runs in gh-only mode without Margins credentials — it still reports missing workflows, stale pins, and over-cap repos from the GitHub API alone (binding-drift checks are skipped when unauthenticated).


Agent / Scripting Mode

All commands support --json for structured output:

margins workspace list --json
# → [{ "slug": "my-repo", "name": "My Repo", "syncStatus": "synced", ... }]

margins discuss list my-repo --json
# → [{ "id": "d_...", "path": "docs/intro.md", "body": "...", "status": "open", ... }]

For non-interactive use (CI, agents), use environment variables instead of stored credentials:

MARGINS_API_KEY=mrgn_... MARGINS_SERVER_URL=https://margins.example.com margins workspace list --json

Exit codes: 0 on success, 1 on any error (auth failure, network error, not found, etc.). Error details are written to stderr.


Local Workspace Config (.margins.json)

When a slug argument is omitted, the CLI walks up from the current directory looking for a .margins.json file. This allows running commands from anywhere inside a repository without repeating the workspace slug.

Example .margins.json:

{
  "workspace_slug": "local/avigano/my-docs",
  "workspace_id": "0cfbdc14-c023-4c84-bc4a-e027e13cefab",
  "default_branch": "main",
  "syncMode": "client",
  "server_url": "https://margins.example.com"
}
Field Description
workspace_slug Default workspace slug for discuss and workspace commands
workspace_id Default workspace UUID. Used by workspace push --workspace for re-pushes — more reliable than slug because it doesn't depend on slug resolution.
default_branch Default branch for workspace sync. For local workspaces this is main; for GitHub-overlay mode (local edits pushed to a GitHub workspace's @local branch) this is @local.
syncMode "client" — the CLI pushes content via workspace push (CAS). "server" — Margins syncs the workspace from a GitHub webhook; the CLI refuses workspace push and directs you to workspace sync. Replaces the legacy mode field ("local" / "overlay"), which is still read and upgraded to syncMode in place.
server_url Server URL override (lower priority than --server-url and MARGINS_SERVER_URL)

Project-scoped credentials: .margins.json is intended to be committed to the repository so teammates share the same workspace identity. It does NOT contain credentials. For project-scoped API keys + server URL, use a .margins/ directory at the project root containing config.json and set MARGINS_CONFIG_DIR to point at it. Add .margins/ to .gitignore since it contains credentials.


Global Config File

The CLI resolves the config directory in this order:

Priority Condition Path used
1 MARGINS_CONFIG_DIR env var is set $MARGINS_CONFIG_DIR/config.json
2 ~/.config/margins/config.json already exists ~/.config/margins/config.json
3 Platform default (fallback) macOS: ~/Library/Preferences/margins/config.json · Linux/XDG: ~/.config/margins/config.json · Windows: %APPDATA%/margins/Config/config.json

Preferred location on all platforms: ~/.config/margins/config.json

If that file exists (e.g. you created it manually, or you're on Linux), it takes precedence over the macOS ~/Library/Preferences/ default. To migrate on macOS:

mkdir -p ~/.config/margins
cp ~/Library/Preferences/margins/config.json ~/.config/margins/config.json

The MARGINS_CONFIG_DIR override is intended for tests and CI — it fully isolates the config from your user profile.

Field Set by Description
apiKey config set-key Static Margins API key (mrgn_...)
serverUrl config set-url Server URL override
accessToken auth login Keycloak JWT access token
refreshToken auth login Keycloak refresh token (auto-refresh)
accessTokenExpiresAt auth login Access token expiry (epoch ms)
keycloakIssuer auth login Keycloak realm URL
keycloakClientId auth login Keycloak client ID

Running margins auth login clears any previously stored apiKey. Running margins config set-key clears any stored Keycloak session.


Development

git clone https://github.com/alvistar/margins-cli.git
cd margins-cli
npm install

# Build
npm run build          # compiles to dist/index.mjs via tsdown

# Run from source (no build required)
npm run dev -- workspace list

# Tests
npm test               # vitest run (114 tests)
npm run test:watch     # watch mode

The CLI is built as ESM. The bin/margins.js shebang entry imports ../dist/index.mjs.

How npx github:alvistar/margins-cli works: npm clones the repo, runs npm install, then runs the prepare script (npm run build) automatically. This compiles src/ to dist/ before the binary is executed — no pre-built files need to be committed.

Related skills

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.