
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-cliAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Last updated | July 27, 2026 |
|---|---|
| Repository | alvistar/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. Callingsyncon a local workspace returnsLOCAL_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
--dirfor.mdfiles (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
mainbranch. - For GitHub workspaces, content lands on the virtual
@localbranch — 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.jsonis 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 containingconfig.jsonand setMARGINS_CONFIG_DIRto point at it. Add.margins/to.gitignoresince 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 loginclears any previously storedapiKey. Runningmargins config set-keyclears 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-cliworks: npm clones the repo, runsnpm install, then runs thepreparescript (npm run build) automatically. This compilessrc/todist/before the binary is executed — no pre-built files need to be committed.