# GoClaw CLI Reference

The GoClaw gateway runs from a single binary. All commands are executed in the format `goclaw <command>`;
`goclaw <command> --help` lists a command's flags.

> **Start Gateway:** `goclaw` (without arguments) → HTTP/WS gateway starts (default port 8080).

---

## Quick Start

```bash
goclaw setup              # Configuration wizard (first time)
goclaw quickstart         # Short setup wizard: which AI and its key, the agent's name and purpose
goclaw                    # Start gateway (after preparing config)
goclaw chat               # LLM chat (does not require gateway)
goclaw chat --debug       # Chat with debug logging
```

---

## Setup & Configuration

```bash
goclaw setup              # Interactive wizard
goclaw quickstart         # Quick wizard (minimal prompts)

# Script-friendly (non-interactive)
goclaw setup --non-interactive \
  --auth-choice gemini-api-key \
  --gemini-api-key $GEMINI_KEY \
  --gateway-port 8080 \
  --gateway-bind lan
```

### `goclaw setup` flags

`--non-interactive` writes a new `config.yaml` from the defaults and these flags, replacing
one that is already there. Without `--gateway-token` it generates a token and prints it.

| Flag | Description |
|---|---|
| `--non-interactive` | Non-interactive mode, writes config directly from flags |
| `--auth-choice <provider>` | `gemini-api-key` \| `openai-api-key` \| `anthropic-api-key` \| `deepseek-api-key` \| `ollama` \| `custom-api-key` |
| `--gemini-api-key <key>` | Gemini API key |
| `--openai-api-key <key>` | OpenAI API key |
| `--anthropic-api-key <key>` | Anthropic API key |
| `--deepseek-api-key <key>` | DeepSeek API key |
| `--custom-base-url <url>` | Custom provider base URL (also the base URL for `deepseek-api-key` and `ollama`) |
| `--custom-model-id <id>` | Custom model ID |
| `--custom-api-key <key>` | Custom provider API key |
| `--model <name>` | Specify model name (ignored for `anthropic-api-key`) |
| `--workspace <path>` | Agent workspace path (default: `~/.goclaw/workspace`) |
| `--gateway-port <port>` | HTTP/WS port (default: 8080) |
| `--gateway-bind loopback\|lan` | Bind network interface (default: `loopback`) |
| `--gateway-token <token>` | Static authentication token |
| `--mode local\|remote` | No effect: the config written is always for a local gateway |
| `--remote-url <url>` | No effect: with `--mode remote` the URL is printed, not saved |
| `--install-daemon` | No effect: prints a reminder to run `goclaw service install` |
| `--skip-skills` | No effect |

---

## Gateway Runtime

```bash
goclaw                    # Start gateway
goclaw --debug            # Start gateway with debug logging
goclaw status             # Status of the running gateway
goclaw doctor             # Run health checks
goclaw version            # View version information
```

One gateway runs per state directory: a second one started on the same directory exits with
`another GoClaw gateway is already running`.

`goclaw doctor` checks the config file, the state, workspace and memory directories, the
secrets vault, the database and its memory schema, each configured integration (datasources,
observability tools, GitLab, SonarQube, MCP servers, enabled channels, mail, and a live call to
the AI provider and to each agent's fallback models), whether the gateway is running and, on
Linux, whether the installed service starts at boot. A line marked `⚠` is a warning, not an
issue: it works today, as an agent with no fallback model does until its provider is down.

`status`, `sessions`, `message` and the `memory` commands talk to the running gateway, which
they find through `<state dir>/gateway.lock`, and send `gateway.auth.token` from config.yaml.
They do not open the secrets vault: a token written as `${secret:NAME}` is sent as written and
refused. `${env:VAR}` works.

---

## Config Management

```bash
goclaw config get ai.provider
goclaw config set ai.provider=gemini
goclaw config set tools.git.allow_write=true
goclaw config append skills.allow=my-skill
goclaw config remove skills.deny=my-skill
goclaw config delete channels.msteams.webhook_port
```

| Command | Description |
|---|---|
| `config get <key>` | Read config value |
| `config set <key=value>` | Set config value |
| `config append <key=value>` | Append value to list config |
| `config remove <key=value>` | Remove value from list config |
| `config delete <key>` | Remove the key itself, comments kept. Takes keys GoClaw does not know, so it clears a typo or a setting the start-up log reports as unknown or removed |

---

## Secrets

An encrypted vault (AES-256-GCM) for the credentials `config.yaml` refers to
with `${secret:NAME}`, so tokens never sit in the config file.

```bash
goclaw secret gen APP_KEY_ERP --provider app_api   # generate one, store, print once
goclaw secret set GEMINI_API_KEY AIza...           # store a value you already have
goclaw secret get GEMINI_API_KEY                   # print the decrypted value
goclaw secret list                                 # all secrets, values masked
goclaw secret list --show-values
goclaw secret delete GEMINI_API_KEY
```

| Command | Description |
|---|---|
| `secret gen <key>` | Generate a random value, store it, and print it once |
| `secret set <key> <value>` | Store a value you supply — takes both arguments |
| `secret get <key>` | Print the decrypted value |
| `secret list` | List names with masked values |
| `secret delete <key>` | Remove a secret |

`secret gen` is for the shared secrets GoClaw asks for but does not issue —
`channels.app_api.api_key`, `jwt_secret`, webhook tokens. Run it once per
calling system to give each its own credential:

```bash
goclaw secret gen APP_KEY_DJANGO --provider app_api
goclaw secret gen APP_KEY_ERP    --provider app_api
```

| Flag | Default | Description |
|---|---|---|
| `--length` | `32` | Random bytes to generate (minimum 16) |
| `--format` | `hex` | Encoding of the stored value: `hex` or `base64` |
| `--category` | `api_key` | `api_key`, `token`, `password`, `credential` |
| `--provider` | — | Owning provider, e.g. `app_api`, `gitlab` |
| `--force` | `false` | Replace an existing secret of the same name |

Regenerating over an existing name needs `--force`: it revokes whatever is
still presenting the old value, and the only copy of that value is gone.

A running gateway picks up vault writes on its own: the secrets page re-reads
the file per request. A `${secret:NAME}` already expanded into the live config
is refreshed on the next config load, so touch `config.yaml` after adding a
secret the config refers to.

> Set `GOCLAW_SECRET_KEY` on a production host before writing to the vault.
> Without it the passphrase is derived from machine and user identifiers — the
> gateway warns about this at startup — and the encryption offers little
> protection against someone who already has access to the host.

See [Secrets architecture](../architecture/secrets.md) for the interpolation
syntax and [App API](channels/app-api.md#one-key-per-calling-system) for issuing
one key per caller.

### Password generator

`goclaw genpass` (also `gp`, `password`) prints strong random passwords and stores nothing.
To generate a value and keep it in the vault, use `secret gen`.

```bash
goclaw genpass                          # profile high: 24 characters, all character classes
goclaw genpass --profile ultra -n 5     # five passwords of 32 characters
goclaw genpass --profile pin -l 8       # an 8-digit PIN
goclaw genpass --no-symbols -l 20       # letters and digits only
```

Profiles: `high` (default, 24), `ultra` (32), `pin` (6 digits), `alpha` (20, no symbols);
`-l`/`--length` changes the length.

---

## Chat

```bash
goclaw chat           # Chat with default agent (does not require gateway)
goclaw chat --debug   # Chat with debug logging
```

---

## Message

```bash
# Requires a running gateway
goclaw message "Hello from CLI"                    # channel: web
goclaw message discord "Deploy production please"
goclaw message msteams "Report is ready"
```

The agent answers the text and the command prints the reply; nothing is posted to the channel.
With a single argument the channel is `web`: the text goes into the Web UI's admin conversation
(`admin-session`) as the admin. With another channel it is answered as a message from a sender
named `cli` on that channel, in the session `p2p:cli:<default agent>`.

---

## Agents

```bash
goclaw agents list
goclaw agents add analyst
goclaw agents bind analyst discord
goclaw agents unbind analyst discord
goclaw agents delete analyst
```

---

## Sessions

```bash
goclaw sessions list
goclaw sessions inspect
```

---

## Memory

The memory commands go through the running gateway and stop with an error when it is not
running. `memory check` and `memory migrate` open the database themselves and need no gateway.
The commands that delete or rewrite records (`clear`, `clear-user`, `forget`, `notes clear`,
`notes purge`, `replay`, `migrate`) first copy the database to
`<state dir>/backups/gc_state_<timestamp>.sqlite`; `dedupe` and `notes delete` do not.

| Command | Description |
|---|---|
| `memory check [--db PATH] [--json]` | Read-only health report: schema migrations, chunk types, oversized chunks, missing embeddings, orphaned and stale chunks. `--db` points it at another database file, a backup for example |
| `memory stats` | Record and search-index statistics |
| `memory reindex` | Rebuild the search index from all memory sources |
| `memory export --file F` / `memory import --file F` | Knowledge catalog (agent notes and agent config) to or from a JSON file |
| `memory replay [channel] [--dry-run]` | Restore Memory.md from session archives |
| `memory relearn [--agent=ID] [--dry-run] [--force]` | Re-extract agent notes from session archives; `--force` also reprocesses archives that have not changed |
| `memory revise [--dry-run]` | Retire learned facts and preferences that a newer one supersedes |
| `memory revisions [--run ID] [--record ID] [--limit N]` | List those decisions, newest first (default 50) |
| `memory revisions undo <memory-id>` or `--run <run-id>` | Restore a retired record, or everything one run retired. The same record does not replace it again |
| `memory backfill-metadata [--batch-size N] [--dry-run]` | Extract topics and entities for records that have none (LLM calls; default 10 records per call) |
| `memory migrate [--dry-run]` | Apply pending memory schema migrations; `--dry-run` lists the statements |
| `memory prune [--stale=DAYS] [--min-len=N]` | Delete search-index chunks: those never read and indexed more than DAYS ago (default 90), and those shorter than N characters (default 80) indexed before the same cutoff. Memory records are not touched |
| `memory dedupe [--ratio=R]` | Delete near-duplicate chunks of the same source, and duplicate agent notes (Jaccard similarity ≥ R, default 0.9) |
| `memory forget [--older=AGE]` | Delete global-scope records created before AGE: `90d` (default), `6m`, `1y`, or a date `2026-01-01` |
| `memory clear [--yes]` | Delete all memory records, every scope |
| `memory clear-user <user-id> [--yes]` | Delete one user's personal records |
| `memory notes list \| grep \| delete \| clear \| purge` | Manage agent notes |

```bash
# Statistics and health
goclaw memory stats
goclaw memory check
goclaw memory check --db ~/.goclaw/backups/gc_state_20260405_120000.sqlite --json

# Export Knowledge Catalog to JSON
goclaw memory export --file catalog.json

# Import Knowledge Catalog
goclaw memory import --file catalog.json

# Rebuild/Reindex memory index
goclaw memory reindex

# Replay/Restore Memory.md from archive
goclaw memory replay discord
goclaw memory replay discord --dry-run

# Relearn agent notes from session archives
goclaw memory relearn                     # main agent (default)
goclaw memory relearn --agent=analyst      # specific agent
goclaw memory relearn --dry-run           # preview what facts would be generated

# Retire learned facts/preferences a newer one supersedes (tagged, never deleted)
goclaw memory revise --dry-run            # preview: each old record, what replaces it, and why
goclaw memory revise                      # apply
goclaw memory revisions                   # what was retired, by what, and why
goclaw memory revisions undo <memory-id>  # put one back

# Cleanup
goclaw memory prune                          # default: stale=90d, min-len=80
goclaw memory prune --stale=60               # chunks never read, indexed more than 60 days ago
goclaw memory prune --min-len=120            # also older chunks shorter than 120 characters

# Delete duplicate chunks and agent notes (Jaccard similarity)
goclaw memory dedupe                         # default ratio=0.9
goclaw memory dedupe --ratio=0.85

# Delete old global SQLite records
goclaw memory forget --older=6m
goclaw memory forget --older=1y
goclaw memory forget --older=2026-01-01

# Clear all memory
goclaw memory clear --yes
goclaw memory clear

# Clear specific user's personal memory
goclaw memory clear-user u_123456 --yes

# Manage agent notes
goclaw memory notes list
goclaw memory notes grep ticket
goclaw memory notes delete 100000000000000105
goclaw memory notes clear --yes
goclaw memory notes purge '%Table%does NOT exist%' --yes
```

`--dry-run` on `prune`, `dedupe` and `forget` only prints a note and exits: it previews nothing.
Look first with `memory check` (stale and oversized chunks) and `memory stats`.

**Recommended Workflow/Sequence:**

```bash
goclaw memory stats               # 1. View current status
goclaw memory check               # 2. Stale, oversized and orphaned chunks (read-only)
goclaw memory notes list          # 3. Check agent notes
goclaw memory notes grep Table    # 4. Find incorrect notes
goclaw memory notes purge '%Table%' --yes  # 5. Delete incorrect notes
goclaw memory prune               # 6. Remove noise
goclaw memory dedupe              # 7. Remove duplicates
goclaw memory forget --older=6m   # 8. Delete old records
goclaw memory reindex             # 9. Rebuild index
```

How the agent uses memory: [Memory and Context](memory_and_context.md). The revision trail:
[Audit and undo](../architecture/memory_policy.md#audit-and-undo).

### Knowledge graph

The graph is kept in `kg.sqlite` in the state directory, or in the file `memory.graph.path`
names. These commands open it and the main database directly.

| Command | Description |
|---|---|
| `graph stats` | Entity, relation and topic counts, next to the entity and relation counts in the main database |
| `graph check` | Graph reachable, tables readable, counts close to the main database's, one-hop traversal and full-text search working |
| `graph migrate [--dry-run]` | Copy memory entities and relations from the main database into the graph. Safe to repeat |
| `graph reindex [--dry-run] [--with-llm] [--scopes=global,personal]` | Sync each memory record's entities and relations into the graph; `--with-llm` also queues Gemini extraction for records that have none |
| `graph cleanup [--dry-run]` | Delete entities no memory mentions, from the main database and the graph |

---

## Database Snapshots

`goclaw db` writes and reads `.gclaw` files: gzip-compressed JSON of the database tables, and
optionally workspace files and wiki attachments. These commands open the database directly.

```bash
goclaw db export -o backup.gclaw                                   # the core tables
goclaw db export -o backup.gclaw --sessions --usage --files --attachments
goclaw db export -o ops.gclaw --agent-id=ops                       # one agent's rows
goclaw db inspect backup.gclaw                                     # manifest and row counts
goclaw db import -i backup.gclaw --dry-run
goclaw db import -i backup.gclaw --replace
```

| Command / flag | Description |
|---|---|
| `db export` | Write a snapshot (default file `goclaw-export_YYYY_MM_DD.gclaw`, or `-o`/`--output`) |
| `--agent-id <id>` | Only that agent's rows in the tables that have an `agent_id` column; other tables are exported whole |
| `--sessions`, `--chunks`, `--executions`, `--usage` | Add conversation history, search-index chunks with embeddings (large), SOP run history, token and voice usage |
| `--files`, `--attachments` | Add workspace markdown files; add wiki attachments with their files (large) |
| `--no-compress` | Write plain JSON |
| `db inspect <file> [--json]` | Print a snapshot's manifest and row counts per table |
| `db import -i <file>` | Import a snapshot. A row whose key already exists is kept unless `--replace`; `--dry-run` writes nothing. What could not be imported is listed, and the command exits non-zero |

The [fleet toolkit](fleet.md) fetches these snapshots from remote agents with `make fetch_db`.

---

## Skills

```bash
goclaw skills list
goclaw skills install github:user/my-skills                      # every skills/**/SKILL.md in the repository
goclaw skills install github:user/my-skills/skills/foo/SKILL.md  # one file, from the main branch
goclaw skills install https://example.com/skills/foo/SKILL.md    # the SKILL.md file itself
goclaw skills install ./my-skill/                                # local directory or file
goclaw skills add github:user/my-skills                          # choose from a list
goclaw skills show git
goclaw skills remove my-skill
```

| Command | Description |
|---|---|
| `skills list` | Loaded skills with their source and status |
| `skills check [name]` | Binary dependencies of every skill; with a name, a compatibility report for that skill |
| `skills scan [name]` | Security scan of all skills, or of one |
| `skills install <source> [--force]` | Install from an `http(s)` URL, `github:user/repo[/path/to/SKILL.md]` or a local path |
| `skills add <github:user/repo> [--force]` | Clone a repository and pick its skills from a list; without a terminal, every compatible skill is installed |
| `skills show <name>` | Skill content and metadata |
| `skills remove <name>` | Remove a user-installed skill |

A URL must point at a `SKILL.md` file: what it returns is saved as the skill, so a repository's
web page (`https://github.com/user/my-skill`) would be saved as HTML. For a repository use
`github:user/my-skill`, which clones it with `git`. Every install, local paths included, runs a
compatibility check first and stops on a FAIL verdict; `--force` installs anyway. Every skill an
`install` or `add` puts in place is then security-scanned, and one with a critical finding is
removed again (the command exits non-zero). A repository with a single `SKILL.md` at its root is
installed under the repository's name. Skills are installed under `<state dir>/skills/`.

---

## Tools — Offline Testing & Debugging

`goclaw tools` allows calling registered tools directly from the CLI **without an LLM**.
There is no need for the gateway to be running — the configuration and SQLite store will load automatically.

### Subcommands

| Command | Description |
|---------|-------------|
| `tools list` | List all registered tools |
| `tools exec <tool> [k=v ...]` | Execute a tool directly and print the full output |
| `tools bench <tool> [k=v ...] [--runs=N]` | Measure tool execution speed |

```bash
# List all tools
goclaw tools list

# Execute tool directly
goclaw tools exec datetime
goclaw tools exec datetime action=diff from=2026-01-01 to=2026-03-30
goclaw tools exec gitlab action=mrs_list project=myorg/backend
goclaw tools exec db_query datasource=main query="SELECT count(*) FROM users"
goclaw tools exec memory_search query='deploy error' max_results=10

# Benchmark execution speed
goclaw tools bench datetime action=now --runs=50
goclaw tools bench db_query datasource=main query='SELECT 1' --runs=20
```

Output Example:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Tool:  datetime
Args:  {"action":"now"}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2026-03-30T08:45:00+08:00
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Time: 1ms | Chars: 25 | Lines: 1
```

### Arg Type Coercion

Values are automatically coerced into types. The shell removes one level of quotes before
`goclaw` sees an argument, so the examples below are written as you type them:

| You type | Tool receives | Description |
|-------|---------------|-------------|
| `limit=5` | number | Every number is a float64, as in a model's tool call |
| `ratio=0.85` | number | Floating-point number |
| `active=true` | `bool` | Boolean |
| `name=hello` | `string` | Simple text |
| `"id='12345'"` | `string` | Quotes that reach `goclaw` keep the value a string, not a number |
| `'filter={"k":"v"}'` | `JSON object` | JSON object; quote it, or the shell strips its quotes |
| `'tags=["a","b"]'` | `JSON array` | JSON array; quote it, or the shell strips its quotes (zsh also reads `[...]` as a glob) |

→ **For detailed examples of all tools**, see the [Tool Execution Reference](./tool-exec-reference.md) page.

---

## Teams Integration

```bash
goclaw teams setup    # Fully interactive configuration wizard

# Package from channels.msteams.* in config.yaml
goclaw teams package

# Or give the values as flags: zip + config
goclaw teams package \
  --app-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  --app-password="YOUR_CLIENT_SECRET" \
  --tenant-id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
  --app-name="GoClaw Bot" \
  --icon-source=./logo.png \
  --out="./teams-app.zip"

# A changed package for an app already in Teams: raise the version
goclaw teams package --app-version=1.0.1
```

Every flag overrides a value from `config.yaml`; only the App ID has to come from one or the
other. `--app-id`, `--app-password` and `--tenant-id` given as flags are also written to
`channels.msteams.*` in config.yaml (`--app-id` sets `channels.msteams.enabled: true` too)
once the package is built, and so are the four publisher flags, under
`channels.msteams.app_package`. A package Teams would refuse (an App ID that is not a GUID, a
version not of the form `1.0.1`, an image that gives no valid icon) is not written.

| Flag | Default | Description |
|------|---------|-------------|
| `--app-id` | `channels.msteams.app_id` | Microsoft App ID of the Azure Bot (required here or in config.yaml) |
| `--app-password` | `channels.msteams.app_password` | Client Secret value |
| `--tenant-id` | `channels.msteams.tenant_id` | Directory (tenant) ID, for a single-tenant bot |
| `--app-name` | `channels.msteams.bot_name`, else `GoClaw Bot` | Bot name in Teams (cut to 30 characters for `name.short`) |
| `--app-version` | `1.0.0` | Manifest `version`; raise it for every changed package of an app already uploaded |
| `--developer-name` | `channels.msteams.app_package.developer_name`, else `Ulab LLC` | Publisher shown on the app's page in Teams, at most 32 characters |
| `--website-url` | `channels.msteams.app_package.website_url`, else `https://goclaw.ulab.mn/` | Publisher's website (`https://`) |
| `--privacy-url` | `channels.msteams.app_package.privacy_url`, else `https://goclaw.ulab.mn/privacy.html` | Privacy policy (`https://`) |
| `--terms-of-use-url` | `channels.msteams.app_package.terms_of_use_url`, else `https://goclaw.ulab.mn/terms.html` | Terms of use (`https://`) |
| `--out` | `./teams-app.zip` | Zip output path |
| `--icon-source` | GoClaw's own icons | PNG, JPEG or GIF file or http(s) URL, resized to the 192×192 color and 32×32 outline icons |
| `--ffmpeg` | — | Deprecated and ignored: icons are resized without ffmpeg |

> ⚠️ **`--app-id`** must be the **Microsoft App ID** from **Azure Bot → Configuration**.

→ Read more: [MS Teams Channel Setup](./channels/msteams.md)

---

## GitLab Webhooks

```bash
goclaw gitlab hook setup                               # wizard: GitLab webhook, GoClaw route and vault secret
goclaw gitlab hook list
goclaw gitlab hook test <key>                          # default trigger: push_events
goclaw gitlab hook test <key> --trigger issues_events
goclaw gitlab hook remove <key>                        # from GitLab and GoClaw
goclaw gitlab hook remove <key> --keep-route           # from GitLab only
goclaw gitlab hook remove <key> --keep-gitlab          # from GoClaw only
```

→ Read more: [GitLab Webhook Integration](./automation/gitlab-webhook.md#command-reference)

---

## Pairing

```bash
goclaw pairing pending [channel]                       # pending requests (default channel: discord)
goclaw pairing list [channel]                          # approved users (default channel: discord)
goclaw pairing approve <CODE>                          # role user
goclaw pairing approve <CODE> --role admin
goclaw pairing revoke <user-id>                        # the u_… ID that approve prints
goclaw pairing link <user-id> <channel> <account-id>   # add a channel account to an existing user
```

> While the gateway is running, approve on the Web UI's **Pairing** tab or through the API.
> The running gateway does not see a pairing change made with the CLI until it restarts, and
> its next write to the pairing files can drop it. See
> [Pairing: REST API](./channels/pairing.md#3-rest-api).

---

## Service (OS Daemon)

```bash
goclaw service install                  # Install per-user systemd / launchd service
sudo goclaw service install --system    # Install system service (runs as your user)
goclaw service uninstall                # Uninstall service
goclaw service start                    # Start service
goclaw service stop                     # Stop service
goclaw service restart                  # Restart service
goclaw service status                   # Show service status
```

`goclaw service install` writes a per-user service; `sudo goclaw service install --system`
writes a system service that still runs as an ordinary user: the one who ran `sudo`, or the
one `--run-as <user>` names. Its data is in that user's `~/.goclaw`. On Linux a per-user
service starts at boot only while lingering is on for its user; install turns it on, or prints
`sudo loginctl enable-linger <user>` when it cannot. An installed service keeps the settings
of the version that wrote it: to write it again, run `goclaw service uninstall` and then
`goclaw service install`. `goclaw doctor` says when an installed service will not start at boot,
and how to fix it. `goclaw service start` on a service that already runs says so and exits 0;
`goclaw service restart` stops it first and starts the new binary. On macOS both also start a
job launchd has loaded but is not running (one that crashed), which `launchctl load` refuses
with "Load failed: 5: Input/output error".

`goclaw service status` says whether the service runs and where its logs are: the gateway's
own log (`~/.goclaw/logs/gateway.log`, or `logging.file.path`), and on macOS what the service
printed (`~/Library/Logs/goclaw.stdout.log` and `goclaw.stderr.log`, where a crash at start-up
shows). On Linux it gives the `journalctl` commands.

**On a Mac, the service can wait for someone to click Allow.** macOS asks at the Mac's screen,
and whoever installs the service over SSH or a terminal does not see it:

- **Local Network** (macOS 15 and later). A per-user service is a launchd agent, which macOS
  stops from connecting to the local network (a database, GitLab or Zabbix on the LAN; the call
  fails with "no route to host") until someone clicks Allow in "goclaw would like to find devices
  on your local network". There is no command to allow it. A system service is a launchd daemon,
  which macOS lets through without asking, and it still runs as you, with your `~/.goclaw`:
  `goclaw service uninstall && sudo goclaw service install --system`. The gateway, started as an
  agent, makes macOS ask at once, and writes `[macos] WARNING` in its log when it is blocked.
- **The firewall**, when it is on. Connections from other machines (the Web UI, webhooks) wait
  for Allow. Allow goclaw from a terminal:
  `sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add <path to goclaw> && sudo /usr/libexec/ApplicationFirewall/socketfilterfw --unblockapp <path to goclaw>`.

macOS sees an unsigned program as new after every update, so both can ask again.
`goclaw service install`, `start`, `status` and `goclaw doctor` say which of them applies to this
Mac, with the command to run.

| | Per-user | `--system` |
|---|---|---|
| Linux (systemd) | `~/.config/systemd/user/goclaw.service` | `/etc/systemd/system/goclaw.service` |
| macOS (launchd) | `~/Library/LaunchAgents/goclaw.plist` | `/Library/LaunchDaemons/goclaw.plist` |

On Linux the service logs to the journal; on macOS to `~/Library/Logs/goclaw.stdout.log` and
`goclaw.stderr.log`.

---

## Update

```bash
goclaw update                     # the latest release
goclaw update --backup            # back up database and state files before updating
goclaw update --version v0.0.96   # a given release
goclaw update --force             # install even when that version is already running
```

`goclaw update` reads the latest version from `https://goclaw.ulab.mn/releases/latest.json`,
downloads the binary for this platform to a temporary file, checks its SHA-256 against the
release's `checksums.json` (and on macOS its code signature, when the binary is signed), keeps
the current binary as `<binary>.prev`, puts the new one in its place and restarts the installed
service. With `--backup`, it first snapshots the SQLite databases (`gc_state.sqlite`, `kg.sqlite`)
and state files (`config.yaml`, `credentials/`, `workspace/`, `agents/`, `memory/`, `skills/`,
`extensions/`, `plugins/`) to
`~/.goclaw/backups/goclaw-backup_<version>_<new-version>_<timestamp>.tar.gz` (keeping the 3 most
recent update archives). When the binary directory (such as `/usr/local/bin`) or a `--system`
service requires root and `goclaw update` was run without `sudo`, it asks for `sudo`; when run as
`sudo goclaw update` on a host with a per-user service, it restarts the invoking user's service
as that user rather than root's. The [fleet toolkit](fleet.md#upgrade-and-restart) runs it on
several hosts.

The site keeps only the newest release. When it does not answer, or does not have the version
asked for, `goclaw update` downloads from the release bucket,
`https://storage.googleapis.com/goclaw-releases/releases/`, which keeps every release from
v0.2.2 on; so `--version` can also go back to an earlier one. The binary is checked against
the `checksums.json` it came with either way.

---

## Transcribe

```bash
goclaw transcribe audio.wav
goclaw transcribe audio.wav --language mn
```

---

## SBOM

```bash
goclaw sbom                                    # summary of the modules compiled in
goclaw sbom -f cyclonedx -o goclaw.cdx.json    # CycloneDX 1.5 JSON
goclaw sbom -f spdx -o goclaw.spdx.json        # SPDX 2.3 JSON
goclaw sbom -f json                            # JSON summary
```

→ Read more: [SBOM & supply chain](../architecture/sbom.md)

---

## Version

```bash
goclaw version
```

Output:
```
goclaw v1.2.3
  commit : abc1234
  built  : 2026-04-03T14:53:00Z
```

---

## Shell Completion

```bash
goclaw completion bash > /etc/bash_completion.d/goclaw
goclaw completion zsh > "${fpath[1]}/_goclaw"
goclaw completion fish > ~/.config/fish/completions/goclaw.fish
goclaw completion powershell
```

---

## Environment Variables

| Variable | Description |
|---|---|
| `GOCLAW_STATE_DIR` | State directory: config.yaml, database, vault, workspace, logs (default `~/.goclaw`) |
| `GOCLAW_HOME` | Home directory to look for `.goclaw` in, when `GOCLAW_STATE_DIR` is not set |
| `GOCLAW_SECRET_KEY` | Passphrase of the secrets vault (see [Secrets](#secrets)) |
| `PORT` | Overrides `server.port` |
| `AI_PROVIDER` | Overrides `ai.provider` |
| `DEBUG` | `1` or `true`: debug logging in the gateway, as `--debug` |
| `GOCLAW_LOG_PROMPTS` | `1` or `true`: write every prompt and response to the log in full. They carry memory and tool results, so credentials and customer data can end up in the log |
| `GOCLAW_DEBUG` | `1`: CLI commands print how they look for the running gateway |

API keys and tokens are set in `config.yaml`, not read from environment variables of their
own. To keep one out of the file, write a reference in its field: `${GEMINI_API_KEY}`,
`${env:VAR}`, `${secret:NAME}` or `${file:/path}` (see
[Configuration Interpolation](../architecture/secrets.md#2-configuration-interpolation)).
Two fallbacks exist: with `ai.gemini.api_key` empty, the Gemini chat client takes
`GEMINI_API_KEY` or `GOOGLE_API_KEY` from the environment, and so do the background model calls
(the session summarizer, fact extraction, the belief-revision judge, consolidation, `memory_replay`,
skill quality checks and the Draft buttons), which ask the default agent's own model when there is
no Gemini key in either place; and memory embeddings take `GEMINI_API_KEY` or `OPENAI_API_KEY` when
`ai.gemini.api_key` or `ai.openai.api_key` is empty.
