Archer

Archer CLI

archer works with Archer documents from the command line. It checks and formats .arch files, keeps a file in sync with a project in your workspace, and runs the MCP server for agents that can only start local servers. The npm package is called archerctl.

check and fmt need no account, so they fit CI and pre-commit hooks. Commands that touch a workspace (push, pull, list, workspace, mcp) sign in the same way agents do and act as you.

The CLI and the MCP server share their code. archer check finds the same problems as the MCP tool validate_source, and archer push saves through create_project and update_project, with the same validation.

Install

Archer needs Node.js 20 or later. Run it without installing:

npx archerctl check

Or install it once and use the archer command:

npm install -g archerctl
archer --version

The examples on this page use archer. With npx, write npx archerctl in its place.

Quick start

archer init                     # architecture.arch, plus the MCP server in .mcp.json
archer check                    # validate every .arch file under this directory
archer login                    # sign in with GitHub in your browser
archer push architecture.arch   # create the project in your workspace

push prints the editor link. Once a file has been pushed, push updates the same project and pull brings the canvas's changes back:

archer push architecture.arch -m "Split the API into two services"
archer pull architecture.arch --force

Commands

archer --help prints a summary of all of them. Options can go before or after the command.

archer check

archer check [paths…] [--json] [--offline]

Validates documents and prints every problem. With no paths it checks every .arch file under the current directory, skipping node_modules, .git, dist and .next. A directory is searched the same way, and a file is checked whatever its extension. - reads a document from stdin.

$ archer check
bad.arch:2:1: warning W301: Unknown type '@my.thing' on 'a' — rendering as a generic component.
bad.arch:3:1: error E002: Duplicate component id 'a' in the top level.
bad.arch:4:1: error E101: Connection target 'ghost' does not resolve to a declared component.
FAILED — 2 files, 2 errors, 1 warning

Each problem is one line on stdout, <file>:<line>:<column>: <severity> <code>: <message>, followed by a summary on stderr. Only errors fail the check: it exits 1 when any file has one, and 0 when there are only warnings. The codes are listed in the language reference.

Option Description
--json Print a machine-readable report instead (see Output formats).
--offline Don't fetch the current workspace's custom types.

Signed in, check first fetches the custom types of your current workspace, so @custom.* tags validate cleanly. Signed out, with --offline, or when the request fails, it checks against the built-in catalog only. An unknown type is just a warning (W301), so this never turns a passing file into a failing one.

archer fmt

archer fmt [paths…] [--check] [--layout] [--drop-comments]

Rewrites documents in canonical form: the form the app saves, with sections in a fixed order, connections sorted and indentation normalized. Paths work as for check. It prints <file>: formatted for each file it changes and nothing for files that are already canonical.

Option Description
--check Change nothing. Print <file>: not formatted for each file that would change, and exit 1 if there are any.
--layout Also complete the layout block, giving every component without a position one from the auto-layout.
--drop-comments Format files that contain # comments.

A # comment isn't part of the document, so canonical form has no comments. Rather than delete them silently, fmt refuses to rewrite a file that has comments unless you pass --drop-comments. A # inside a string or a payload isn't a comment.

fmt also refuses a file the parser rejects, with a syntax error (E001) or a second shape, icon or color in one component (E004), because formatting it would lose part of the file. Other errors, like an unresolved connection, don't stop it. Either refusal is printed on stderr and makes fmt exit 1.

With - as the path, fmt reads stdin and writes the formatted document to stdout:

archer fmt - < draft.arch > architecture.arch

archer init

archer init [file] [--title <title>] [--no-mcp]

Starts a project in the current directory:

  • Writes a starter document to architecture.arch, or to file, which must end in .arch. The starter is valid, canonical, and uses each construct once. An existing file is never overwritten.
  • Adds the hosted MCP server to .mcp.json as archer (see Files), so agents that read that file find it. Other servers in the file are kept, and so is an existing archer entry. --no-mcp skips this step.

The document's title is --title, or the directory's name in title case (my-shop becomes My Shop).

$ archer init --title "My Shop"
architecture.arch: created
.mcp.json: created (MCP server "archer" → https://tryarcher.dev/mcp)

Next:
  archer check             validate every .arch file here (exit 1 on errors; --json for machines)
  archer guide             the language specification
  archer login             sign in with GitHub (in the browser), then: archer push architecture.arch

archer guide

archer guide

Prints the Archer language specification as markdown. Hand it to an agent that has a shell but no MCP client: archer guide > ARCHER.md.

archer login

archer login [--no-browser]

Signs you in with GitHub, in the browser. This is the standard MCP sign-in, the same one an agent goes through when it connects to the server: the CLI registers itself as "Archer CLI", opens the consent page, and receives the result on a localhost address it listens on for the next five minutes.

$ archer login
Sign in to https://tryarcher.dev in your browser:
  https://…/oauth/authorize?response_type=code&client_id=…
Signed in to tryarcher.dev — workspace Personal (owner)

The sign-in page opens in your default browser, and its address is printed too in case it doesn't. --no-browser only prints it. The browser has to be able to reach localhost on the machine running archer: see Signing in on a remote machine.

The session is saved in ~/.archer/auth.json and renewed automatically. A new login replaces it. What the CLI saves appears in the project's history as "Archer CLI".

archer logout

archer logout

Revokes the session and deletes it from ~/.archer/auth.json. Prints Signed out of <app>, or Not signed in. if there was no session.

archer workspace

archer workspace [id] [--json]

With no id, lists your workspaces: Personal first, then the teams you belong to, with your role in each. * marks the current one, which push, pull and list act on.

$ archer workspace
* user_2wKx9TqLm4RbVc7NpZ3hJd8sFyA  Personal  owner
  jn7dt5pzq1b3wk8m2c9x4v6hre0fs7ya  Acme  editor
$ archer workspace jn7dt5pzq1b3wk8m2c9x4v6hre0fs7ya
Now using Acme (editor) — 4 projects.

With an id, it switches. The current workspace is stored with your account, not on this machine: switching here also switches every agent you've connected, and switching in an agent (the MCP tool use_workspace) switches the CLI. As a viewer you can pull and list, but push is refused.

archer list

archer list [--json]

Lists the projects in the current workspace, most recently updated first: one <id> <title> line each.

$ archer list
k57bmtyf4pmj3fmrbrz9hq7qwn6ypcp5  Shop
k1792cxn5wm0fq8hq6dhz4v3sr7b2eay  Link Shortener

archer push

archer push <file> [--project <id>] [-m <summary>]

Saves a file to your workspace. The first push of a file creates a project and records the link in archer.json. After that, push updates the linked project. --project updates a particular project instead, and links the file to it.

$ archer push shop.arch -m "First cut"
Created "Shop" — https://tryarcher.dev/documents/k57bmtyf4pmj3fmrbrz9hq7qwn6ypcp5
$ archer push shop.arch -m "Add a cache"
Updated "Shop" — https://tryarcher.dev/documents/k57bmtyf4pmj3fmrbrz9hq7qwn6ypcp5

The file is checked first. If it has errors, nothing is saved:

$ archer push bad.arch
bad.arch:3:1: error E101: Connection target 'ghost' does not resolve to a declared component.
REJECTED — nothing was saved. Fix the errors and push again.

Warnings are printed but don't stop the push.

Option Description
--project <id> The project to update (ids come from archer list).
-m, --message <summary> One line for the project's history, next to "Archer CLI". A first push without one reads "Created the project".

The project is saved in canonical form and laid out, and the file on disk is left as you wrote it. Components that already exist keep their places on the canvas, and new ones are placed automatically. If the file has a layout block, its positions win over the canvas's: see Round trips with the canvas.

A project stored in a repository file is different: the file in the repository is the source of truth, and push only updates the project's unpublished draft (it says so). See Projects stored in a repository.

archer pull

archer pull <file> [--project <id>] [--force]

Writes the project's saved document to the file, and links the file to the project. Without --project, it pulls the project the file is linked to. Pulling into a new file needs --project:

archer pull shop.arch --project k57bmtyf4pmj3fmrbrz9hq7qwn6ypcp5

If the file already exists and differs from the project, pull stops without changing it. --force overwrites it. The pulled document is canonical and ends with the layout block from the canvas, except for a project stored in a repository file: that one is pulled without its layout block, exactly as the app publishes it, so the result is ready to commit.

$ archer pull shop.arch
archer: shop.arch differs from the project — pass --force to overwrite it.
$ archer pull shop.arch --force
shop.arch: pulled "Shop"

archer mcp

archer mcp

Runs the Archer MCP server over stdio, for MCP clients that can only start local servers. It passes every message on to the hosted server using your archer login session, so the tools are exactly the hosted ones and nothing runs locally. Register it with your client as the command archer mcp (or npx -y archerctl mcp); the MCP docs have examples.

stdout carries the protocol, and log messages go to stderr. When you're not signed in, every request is answered with an error that says to run archer login. The relay reads your session for each request, so once you sign in, from any terminal, the next request works; reconnect the server in your client if it gave up. The relay runs until the client closes its input.

Global options

Option Description
-h, --help Print usage, including the app the CLI talks to. archer help does the same.
-v, --version Print the version.

Files

archer.json

Links between local files and projects, written by push and pull in the directory you run them from:

{
  "projects": {
    "architecture.arch": "k57bmtyf4pmj3fmrbrz9hq7qwn6ypcp5"
  }
}

Paths are relative to that directory, so run archer from the same place (usually the repository root). Commit the file so your teammates push to the same projects. It holds only ids, never credentials. A link only works in the workspace its project belongs to, so a team project needs everyone on the team's workspace (archer workspace <id>).

.mcp.json

archer init registers the hosted MCP server in the project's .mcp.json, the file Claude Code and other MCP clients read:

{
  "mcpServers": {
    "archer": {
      "type": "http",
      "url": "https://tryarcher.dev/mcp"
    }
  }
}

It holds no credentials either. Each person, and each agent, signs in for themselves the first time they connect.

~/.archer/auth.json

Your sign-in, one entry per server: the CLI's client registration, the tokens, and the server's sign-in settings. Only you can read it (mode 0600, in a 0700 directory). It is only ever sent to the server it came from. logout deletes the tokens and keeps the registration for the next login.

Output formats

Diagnostics. check and push print one line per problem:

architecture.arch:4:1: error E101: Connection target 'ghost' does not resolve to a declared component.

The line and column are left out when a problem has no position. Documents read from stdin are named <stdin>.

check --json prints one report for all the files:

{
  "valid": false,
  "files": [
    {
      "file": "bad.arch",
      "valid": false,
      "errors": [
        {
          "code": "E101",
          "severity": "error",
          "message": "Connection target 'ghost' does not resolve to a declared component.",
          "line": 4,
          "col": 1
        }
      ],
      "warnings": []
    }
  ]
}

valid is false when any file has an error. line and col are only present when the problem has a position. The exit code is the same as without --json.

list --json prints the projects as [{ "id", "title", "updated" }], with updated as an ISO timestamp. workspace --json prints [{ "id", "name", "kind", "role", "current" }], where kind is personal or team and role is owner, editor or viewer.

Exit codes

Code Meaning
0 Success.
1 The documents have problems: check found errors, fmt --check found unformatted files, fmt refused a file, or push was rejected.
2 The command couldn't run: a usage mistake, a missing file, no .arch files found, an invalid archer.json or .mcp.json, not signed in, a network failure, or an error from the server (no such project, not a member of the workspace, a viewer pushing). The reason is printed on stderr, after archer:.

Environment

Variable Description
ARCHER_APP_URL The Archer app to use. Defaults to https://tryarcher.dev. The MCP server is $ARCHER_APP_URL/mcp, and sign-ins are kept per server, so a different app needs its own archer login. archer --help shows the current value.

Point it at another deployment, or at a local development server:

ARCHER_APP_URL=http://localhost:3000 archer login

Recipes

Check documents in CI

check and fmt need no sign-in. In GitHub Actions:

name: Architecture
on: [push, pull_request]
jobs:
  archer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npx -y archerctl check
      - run: npx -y archerctl fmt --check

Drop the fmt --check step if you don't keep files in canonical form. Documents pulled from the app always are. push needs a person's sign-in, so it doesn't belong in CI.

Check before each commit

A Git pre-commit hook that checks the staged .arch files:

#!/bin/sh
files=$(git diff --cached --name-only --diff-filter=ACM -- '*.arch')
[ -z "$files" ] || npx -y archerctl check $files

Round trips with the canvas

A file and its project can both change: you edit the file, and people and agents edit the project on the canvas. push sends your file, and pull --force takes the project's version. Neither merges, so pull before you edit when the canvas may have changed.

Positions work differently. push keeps the canvas's positions for every component that already exists, unless your file has a layout block: the block's positions replace the canvas's. A pulled file ends with a layout block, so pushing it later puts the components back where they were when you pulled. To send only the architecture, delete the layout block before pushing.

Projects stored in a repository

A project can instead be stored in a .arch file in its GitHub repository (created From GitHub in the app, or moved there with Move to GitHub). That file, on its branch, is the source of truth: edit it and commit it with the code it describes, and gate it with the CI steps above. The canvas follows the branch, and edits made on the canvas come back to the repository as pull requests. There's nothing to push: push to such a project only updates its unpublished draft, and pull writes the architecture without the layout block, ready to commit.

Agents with a shell but no MCP

An agent can use Archer through the CLI alone. It learns the language from archer guide, writes the document, and checks it with archer check --json until the report is clean. You then archer push it, or the agent does, with your sign-in.

Team projects

Switch to the team's workspace, then push:

archer workspace                 # find the team's id
archer workspace jn7dt5pzq1b3wk8m2c9x4v6hre0fs7ya
archer push architecture.arch -m "Initial architecture"

Commit archer.json. Teammates switch to the same workspace, then pull and push the same project.

Troubleshooting

Not signed in to … Run: archer login. You haven't signed in to this app, or the session expired or was revoked. Run archer login. The message names the app: sessions are per ARCHER_APP_URL.

The browser didn't open. Open the address login printed.

Timed out waiting for the browser sign-in. login waits five minutes for the browser. Run it again.

No project with id … in the current workspace. The project is in another workspace, or was deleted. Check archer workspace and archer list. If the project is gone, remove its line from archer.json and push to create a new one.

… differs from the project — pass --force to overwrite it. The file and the project differ. That includes a hand-written file that is merely not in canonical form. --force replaces the file with the project's version.

… has # comments, which formatting removes. Pass --drop-comments, or put the notes in desc or a markdown payload, which are part of the document.

This needs editor access; you are a viewer. You're a viewer in the current workspace. Ask an owner to make you an editor, or push to a workspace of your own.

check is slow without a network. Signed in, check asks the server for your custom types, and waits up to 30 seconds before giving up. Pass --offline.

Signing in on a remote machine

archer login listens for the browser on localhost, on the machine where it runs. Over SSH or in a container, your browser can't reach that address: after you approve, it shows a connection error. Copy the address from the browser's address bar (it starts with http://localhost:), and while login is still waiting, open it on the machine running archer:

curl 'http://localhost:43411/callback?code=…&state=…'

login then finishes as usual. Alternatively, forward the port before approving: its number is in the printed address's redirect_uri.