# Script the CLI

> Read graft's output from a script or an agent: JSON on stdout, errors as JSON on stderr, and an exit code that says what kind of failure it was.

*Run graft from a script or an agent and read exactly what happened.*

Every graft command works the same way when a program runs it instead of a
person. The data goes to stdout, messages go to stderr, and the exit code says
whether it worked and, if not, what kind of failure it was. This page covers
those rules. The [CLI reference](/docs/cli-reference) lists every command and
flag.

## Find a command and its flags

`graft --help` lists the commands by task. `graft <command> --help` shows one
command's usage, flags and examples, even when the rest of the line is
incomplete. `graft help <command>` prints the same thing.

```sh
graft help branch create
graft help exit-codes
```

The help topics are `exit-codes`, `json`, `environment` and `auth`.

An agent can learn the whole CLI in one call. `graft commands --json` prints
every command with its usage, arguments, flags, whether it takes `--json`,
and whether it needs a database.

```sh
graft commands --json
```

A command only takes the flags it declares. A misspelt or misplaced flag stops
the command before it does anything, names the closest match, and exits 2:

```text
graft: unknown option "--jsno" for graft compile.
Did you mean `--json`? Run `graft compile --help`.
```

## Get JSON instead of text

Every command that prints data takes `--json`. It then prints one JSON object
on stdout and nothing else. Progress and messages still print, on stderr, so a
script can save stdout and a person can still read the log. The object is
indented on a terminal and on one line when piped or redirected.

```sh
graft check --json
```

```json
{"documents":1,"problems":[]}
```

Each command's entry in the [CLI reference](/docs/cli-reference) says whether
it takes `--json`. Commands that run until you stop them (`graft dev`,
`graft serve`, `graft studio`, `graft mcp`) do not.

## Read and write content from the terminal

The `graft content` commands run the same tools an agent calls over the Model
Context Protocol (MCP), the open standard that lets agents use tools. They run
them in the CLI's own process, so they check the schema, compile, and file
approvals exactly as the agent's call would, and need no database on the
static engine.

Print a document as the file it is, change it, and write it back:

```sh
graft content get pages/home > home.mdx
graft content write pages/home --file home.mdx
```

```text
Wrote pages/home.mdx on branch "main": +0 added, ~1 changed, -0 removed.
```

`--file -` reads the document from stdin. To write frontmatter alone, pass it
as JSON with `--data '{"title":"About"}'`, and the body with `--body-file`.

Find documents, and change one field on several at once:

```sh
graft content search pricing --collection pages
graft content set pages home about --field tagline --value '"Updated"'
```

`graft content set` writes each document the way `graft content write` does.
One that fails is listed with its error and fix, the others are still saved,
and the command exits 1.

Deleting needs a person. The first run files an approval and exits 3, and its
`fix` is the exact command to repeat:

```text
error DESTRUCTIVE_OP_REQUIRES_APPROVAL: "delete_content" is destructive and always requires human approval. An approval request has been filed.
  fix: Ask a person to run `graft approve f9cb72bb-4b39-48e7-913b-9b9979da310e`, then run `graft content delete pages/about --approval f9cb72bb-4b39-48e7-913b-9b9979da310e`. The approval works once, for this exact call.
```

Approvals live in Postgres, so on the static engine `graft content delete`
answers `NEEDS_DATABASE`: delete the file and compile instead.

`graft schema` prints each collection's fields, `graft fn run` runs a typed
function, `graft drafts` handles drafts where content is kept in the
database, and `graft explain <code>` explains any error code. `graft mcp call`
runs any other tool by name. The [MCP tools](/docs/mcp-tools#call-a-tool-from-the-terminal)
page maps each tool to its command.

## Work with a hosted site

The same commands work on a site you host with `graft serve --studio`, through
its `/api/mcp` endpoint. Make a token in Studio under **Developer, Agents**,
then save it once:

```sh
printf %s "$TOKEN" | graft login https://cms.example.com --with-token
```

`graft login` checks the token with the site, prints who it acts as and what it
may do, and saves it in `~/.config/graft/hosts.json` (under `XDG_CONFIG_HOME`
when that is set), readable by you alone. Without `--with-token`, on a terminal,
it asks for the token without echoing it. A token is never an argument, where it would
land in your shell history.

Then add `--remote` to a content command:

```sh
graft content list --remote
graft content write posts/launch --file launch.mdx --remote
```

Each prints the site it is using on stderr. In CI, set `GRAFT_URL` and
`GRAFT_TOKEN` in the environment instead: commands that take `--remote` then
use the site without the flag, and `--local` brings them back to the folder's
project. `GRAFT_URL` is never read from `.env`, so a file in some folder cannot
point a command at a production site.

`graft auth status` checks every saved token against its site, and
`graft logout` forgets one on this computer. A token keeps working until
someone revokes it in Studio.

No token can approve an action. A delete over `--remote` exits 3, and a person
approves it in the site's Studio before you repeat it with
`--remote --approval <id>`.

### Call the Studio API

`graft api` calls any [Studio API](/docs/studio-api) route on the site with the
saved token, so you never paste the token into a curl command:

```sh
graft api workspace
graft api entries -f collection=posts --paginate
graft api -X PUT entry --input entry.json
```

A path is under `/api/studio/v1` unless it starts with `/api/fn` or
`/api/content/v1`. `-f key=value` sends a string and `-F key=value` sends
`true`, `false`, `null` or a number as JSON. Fields make the request a POST
with a JSON body; with `-X GET` or `--paginate` they become the query string.
`--paginate` follows `nextCursor` and prints every page. Add `--slurp` to get
every page as one JSON array once the last one arrives. `graft api` with no
path lists the routes.

The answer prints on stdout. A refused request also prints its error and fix
on stderr and exits 1, or 4 when the token lacks the access. `-X DELETE` asks
before it sends anything on a terminal, and needs `--yes` everywhere else.
`graft api` never follows a redirect, and never sends the token to another
site.

## Read a failure

When a command run with `--json` fails, stderr gets one line of JSON with the
same fields every Graft error has: the `error` code, a `message`, the `fix`
that names the next step, and `details` when there are any. Stdout stays
empty.

```json
{"error":"USAGE_INVALID","message":"unknown option \"--jsno\" for graft compile.","fix":"Did you mean `--json`? Run `graft compile --help`.","details":{"command":"compile","flag":"--jsno","suggestion":"--json"}}
```

Without `--json`, the same error prints for a person:

```text
error CONFIG_NOT_FOUND: No graft.config.{ts,js} found in /tmp or any parent directory.
  fix: cd into a Graft project, or scaffold one here with `graft init`.
```

The [error reference](/docs/errors) explains every code.

## Act on the exit code

| Exit code | What it means | What to do |
| --- | --- | --- |
| `0` | It worked. | Nothing. |
| `1` | It failed. | Read the error and its `fix`. |
| `2` | The command was called wrong. Nothing ran. | Fix the command; retrying it as it is fails the same way. |
| `3` | A person has to approve the action first. | A person runs `graft approve <id>`, then you repeat the action. |
| `4` | The credential is missing, wrong, or lacks the access this needs. | Fix the credential before retrying. |

`graft doctor`, `graft check` and `graft ci skip-build` use their own codes,
listed in their reference entries. `graft ci skip-build`, for example, exits 0
when the deploy can be skipped and 1 when it has to build.

## Print what a compile changed

`graft compile --json` prints the branch, the commit and what changed. The
summary line goes to stderr. It works on both engines.

```json
{
  "branch": "main",
  "gitSha": "b8eb719a468be404bae9adca4fbea4d90c642411",
  "changes": {
    "added": ["posts/launch"],
    "changed": ["pages/home"],
    "removed": [],
    "unchanged": 42
  }
}
```

`added`, `changed`, and `removed` are lists of `collection/slug` ids, and
`unchanged` is a count. `gitSha` is `null` when the content is not in a git
repository.

On the static engine, a content change ships with a new deploy, so there is
nothing to refresh. On the Postgres engine, writes from agents, Studio,
`graft merge` and `graft migrate` call your app's revalidate route when
`GRAFT_REVALIDATE_URL` and `GRAFT_WEBHOOK_SECRET` are both set. With the URL
alone, these commands refuse to start. `graft compile` does not, because a deploy
compiles before the new version is live. Send its output to the route yourself
so the app refreshes only the pages that changed. A shell does not read `.env`,
so export `GRAFT_WEBHOOK_SECRET` in the script's environment first:

```sh
graft compile --json > changes.json || exit 1
curl --fail -X POST https://example.com/api/revalidate \
  -H "Authorization: Bearer $GRAFT_WEBHOOK_SECRET" \
  -H "content-type: application/json" \
  --data-binary @changes.json
```

See [Refresh pages when content changes](/docs/next#refresh-pages-when-content-changes)
for the route.

## Stop the CLI from asking questions

Only `graft create` and `graft db claim` ask questions, and only on a
terminal. Pass `--no-input`, or set `GRAFT_PROMPT_DISABLED=1` for every
command at once, and they never ask. `graft create` then takes the default
for each question a flag did not answer. `graft db claim` says to run it again
with `--url`.

## Turn color off or on

graft prints color only to a terminal. `NO_COLOR` turns it off everywhere.
`TERM=dumb` and `CLICOLOR=0` turn it off too. `FORCE_COLOR` or
`CLICOLOR_FORCE` turn it on when the output is not a terminal. Set
`GRAFT_DEBUG=1` to print the stack trace of an unexpected error.

## Update notice

When a newer Graft is on npm, `graft` prints a notice on stderr after the
command finishes. `graft dev`, `graft serve` and `graft studio` print it while
they run. It names the new version, the version you have, and the
[upgrade guide](/docs/upgrading). On a beta, the notice is yellow, because beta
releases often include security fixes. On a stable release it is cyan.

The CLI asks the registry at most once a day and caches the answer in
`~/.cache/graft/update-check.json` (or under `XDG_CACHE_HOME`). It uses the
registry in `npm_config_registry` when you set one. A slow or unreachable
registry never fails or delays a command by more than 1.5 seconds.

The check is off when `GRAFT_NO_UPDATE_CHECK=1` or `CI` is set, under
`--json`, and for `graft mcp`, `graft help`, `graft commands`, `--help` and
`--version`. `GRAFT_NO_UPDATE_CHECK=1` works in the environment or in the
project's `.env`.

## Next steps

- [CLI reference](/docs/cli-reference): every command and flag.
- [Error reference](/docs/errors): what each error code means and how to recover.
- [Connecting agents](/docs/connecting-agents): give an agent the same operations over MCP.
