---
name: cli
description: Install the firmfact CLI (`firmfact`), sign up from a terminal with an emailed code or sign in through the browser, upload invoices and contracts to be read, and query workspaces from scripts with --json output and meaningful exit codes.
---

# firmfact CLI

`firmfact` is the command-line client for firmfact: licence management for complex software and data subscriptions (market data vendors such as Bloomberg, LSEG and FactSet). Its look-up and analysis commands are the MCP tools (`list_vendors` is `firmfact vendors list`, `analyze_cost_trends` is `firmfact analyze cost-trends`), so they read the same account-scoped data as an MCP client, with the signed-in user's permissions. `firmfact upload` also sends documents through the web app's intake.

Documentation: `/cli`. Source (Apache License 2.0): https://github.com/firmfact/cli

## Install

macOS, with Homebrew:

```bash
brew install firmfact/tap/firmfact
```

Linux (also a Mac with OpenSSL 3 or a signed-in GitHub CLI):

```bash
curl -fsSL https://firmfact.com/install.sh | sh
```

Windows 11 or Windows Server 2022 and later, in PowerShell:

```powershell
irm https://firmfact.com/install.ps1 | iex
```

Any version of Windows, Windows 10 included, with Scoop:

```powershell
scoop bucket add firmfact https://github.com/firmfact/scoop-bucket
scoop install firmfact
```

Any platform with Go 1.27.1 or newer:

```bash
go install github.com/firmfact/cli/cmd/firmfact@latest
```

Both scripts check that the release comes from firmfact and matches its checksum before they install anything, and stop if they cannot. On Linux and macOS the signature check needs OpenSSL 3; without it, a signed-in GitHub CLI (`gh`) 2.49 or later checks GitHub's build attestation instead. A Mac has neither out of the box, so use Homebrew there. To pin a version, set the variable for the shell that runs the script, not for curl:

```bash
curl -fsSL https://firmfact.com/install.sh | FIRMFACT_VERSION=0.1.0 sh
```

```powershell
$env:FIRMFACT_VERSION = '0.1.0'; irm https://firmfact.com/install.ps1 | iex
```

As root (in a container, for example), also set `FIRMFACT_INSTALL_DIR` to a directory only root can change, such as `/usr/local/bin`: `curl -fsSL https://firmfact.com/install.sh | FIRMFACT_INSTALL_DIR=/usr/local/bin sh`. By hand: download the archive from https://github.com/firmfact/cli/releases, then `gh attestation verify firmfact_<version>_linux_amd64.tar.gz --repo firmfact/cli --cert-identity https://github.com/firmfact/cli/.github/workflows/release.yml@refs/tags/v<version>`.

## Sign up or sign in

- New account: `firmfact signup`. Work email, name and organisation; a 6-digit code is emailed to that address; entering it creates the organisation's own workspace plus a Demo workspace with clearly marked sample data, and signs the CLI in. The person who owns that address reads the code and accepts the terms. A signup from a domain not yet known is reviewed by a person first, and the code follows once it is approved.
- Scripted signup runs twice: first with the details as flags (`firmfact signup --help` lists them), then `FIRMFACT_SIGNUP_CODE=<code> firmfact signup --email <address>`. Never pass a password or the code as an argument; use `--password-stdin` and the environment variable.
- Existing account: `firmfact login` opens the browser (OAuth 2.1 authorization code + PKCE, loopback redirect). SSO organisations and two-factor users always finish here.
- `firmfact logout` revokes the sign-in on the server and removes it locally.

## Query

```bash
firmfact workspaces list --json
firmfact vendors list --json --workspace <name or id>
firmfact contracts list --query Bloomberg --json
firmfact analyze cost-trends --entity-type vendor --entity-name LSEG --json
firmfact ask "Which contracts renew in the next 90 days?" --json
firmfact contract-items list --all --format csv --columns name,userdef_id,monthly_cost
```

- `--help` on any command lists its flags and accepted values; `firmfact tools list --json` lists every tool.
- `ask` saves a chat thread in the workspace; `--continue` follows up in it and `data.thread_id` carries its id. The look-up and analysis commands read; `upload` (below) sends documents.

## Upload documents

```bash
firmfact upload ~/Invoices/2026-09 --recursive --workspace <name or id> --json
firmfact upload invoice.pdf usage-report.xlsx --related --workspace <name or id>
firmfact upload status --json
```

- Sends invoices, contracts, order forms and spreadsheets through the same intake as the web app's upload page. It first shows what is new, already in the workspace or refused; files already there are not sent again. `--related` sends files as one group, such as an invoice and its usage report.
- `data.results` has an entry per file: its `outcome` (such as `created`, `duplicate` or `refused`) and the `document`, with what was `read`, the `contract_match`, a `variance` preview and the `review`, with its `url`. For a spreadsheet or an HR file, `document.read.records` has an entry per record type (`type`, `label`, `total` and each count). Nothing is booked until a person publishes the document in the web app.
- Needs an admin or editor role in the workspace, on every plan. Name the workspace: without `--workspace`, an upload that would go to the Demo workspace exits 2 off a terminal unless `--yes` is given. `--no-wait` returns once the files are sent; `firmfact upload status <id> --wait` waits for the reading.
- A few kinds of file can only be uploaded in the web app for now; the CLI refuses them with a message saying so. `-` reads one file from standard input and needs `--name` with its file name and extension.

## AI assistants with a terminal

Claude Code, Codex, GitHub Copilot, Cursor and other AI assistants with a terminal run these commands themselves. `firmfact connect claude-code` registers the firmfact connector in Claude Code and adds a firmfact skill there that describes the CLI. An assistant with the connector alone gets the commands for a task, such as an upload or a whole-list export, from the connector's `cli_commands` tool. Connector: `/connector` and the `mcp-integration` skill.

## Output

`--json` prints one object on stdout: `{"data": ..., "meta": {"page", "total_pages", "total_count", "truncated"}, "notes": [...]}`. `meta.workspace_data_source` marks Demo sample data. With `--all --json`, rows stream as NDJSON. Progress, notes and the sign-in link go to stderr.

## Exit codes

| Code | `status` | Meaning |
|------|----------|---------|
| 0 | | success |
| 1 | `failed` | any other failure, such as a file an upload refused or could not read |
| 2 | `usage` | wrong command line |
| 3 | `not_signed_in` | run `firmfact login` |
| 4 | `not_found` | no such workspace, profile, tool or record |
| 5 | `unavailable` | the service is busy or unreachable, or the wait for uploaded documents to be read ran out; retry later |
| 6 | `unsupported` | the host cannot serve this CLI version |
| 7 | `not_allowed` | an administrator of the workspace has switched the firmfact CLI off there |
| 9 | `variance_exceeded` | a finding, not a failure: `upload` or `upload status` with `--fail-on-variance` (`=2%` of the contracted amount, `=50` in the invoice's currency, or no value for any variance) read every document, and an invoice's variance preview is over the threshold |
| 130 | `interrupted` | Ctrl-C or SIGTERM |

With `--json`, errors are one line on stderr: `{"error": {"message", "code", "status"}}`.

## Plans

Uploading documents works on every plan, including the free Starter plan. The Demo workspace works on every plan. Looking up the organisation's own workspace data needs Professional or above, the plans with API and MCP access; `list_workspaces` says whether each workspace's plan includes it. Pricing: `/pricing`.
