# Scaffold and pull a config

> Write a starter firewall.config.json with init, or fetch the live config with pull and --output.

- Repository: jaredpalmer/vwaffle
- GitHub: https://github.com/jaredpalmer/vwaffle
- Human docs: https://grok-wiki.com/public/docs/jaredpalmer-vwaffle-7983cb893581
- Complete Markdown: https://grok-wiki.com/public/docs/jaredpalmer-vwaffle-7983cb893581/llms-full.txt

## Source Files

- `src/index.ts`
- `src/api.ts`
- `src/config.ts`
- `README.md`

---

---
title: "Scaffold and pull a config"
description: "Write a starter firewall.config.json with init, or fetch the live config with pull and --output."
---

`vwaffle init` writes a local starter `firewall.config.json` from the hardcoded `STARTER_CONFIG` object and never calls Vercel. `vwaffle pull` resolves project context, `GET`s `/v1/security/firewall/config/active`, unwraps an `active` envelope when present, and either prints tab-indented JSON to stdout or writes `--output FILE`.

## Choose init or pull

| | `init` | `pull` |
| --- | --- | --- |
| Source | Built-in `STARTER_CONFIG` | Live Vercel Firewall config |
| Auth / project | Not required | `VERCEL_TOKEN` plus a project ID |
| Default path | `firewall.config.json` via `-c, --config` | stdout unless `-o, --output FILE` |
| Parent directories | `mkdir(..., { recursive: true })` | Not created |
| Existing file | Refuses: `<path> already exists; refusing to overwrite.` | Overwrites `--output` |
| Secrets | None in the starter | Writes live values as returned; no `${VAR}` interpolation or `[REDACTED]` |
| Success stdout | `Wrote <path>. Edit it, then run \`vwaffle plan\`.` | `Wrote active firewall configuration to <file>.` or the JSON itself |

Use `init` for a new versioned file. Use `pull --output firewall.config.json` when the dashboard already holds the policy you want to version. Both commands resolve relative paths against `process.cwd()`.

<Note>
`--config` is the write target for `init` only. `pull` ignores `--config` and writes only to stdout or `--output`. `--project`, `--team`, `--yes`, `--dry-run`, and `--check` are unused by `init`.
</Note>

## Prerequisites

`init` needs a writable working directory. `pull` needs context from `resolveContext`:

| Value | Resolution order |
| --- | --- |
| Token | `VERCEL_TOKEN` (required) |
| Project | `--project`, then `VERCEL_PROJECT_ID`, then `.vercel/project.json` `projectId` |
| Team | `--team`, then `VERCEL_TEAM_ID`, then `.vercel/project.json` `orgId` (optional) |

Missing token:

```text
VERCEL_TOKEN is required. Create a Vercel API token (https://vercel.com/account/tokens) and export it before running this command.
```

Missing project:

```text
A project is required. Pass --project, set VERCEL_PROJECT_ID, or run `vercel link` so .vercel/project.json exists.
```

CLI failures print `vwaffle: <message>` on stderr and set `process.exitCode = 1`.

## Scaffold with init

<Steps>
<Step title="Write the starter file">

```sh
vwaffle init
```

Or a non-default path (parent directories are created):

```sh
vwaffle init --config path/to/firewall.config.json
```

</Step>
<Step title="Confirm it was written">

Stdout:

```text
Wrote firewall.config.json. Edit it, then run `vwaffle plan`.
```

The file is `JSON.stringify(..., null, '\t')` plus a trailing newline.

</Step>
<Step title="Handle an existing file">

If the target already exists, `init` does not overwrite:

```text
vwaffle: firewall.config.json already exists; refusing to overwrite.
```

Pick a new `--config` path, or start from live state with `pull --output` instead.

</Step>
</Steps>

### Starter body

`STARTER_CONFIG` is the exact object written:

```json
{
	"firewallEnabled": true,
	"managedRules": {
		"owasp": { "active": false }
	},
	"rules": [
		{
			"name": "Block sensitive paths",
			"description": "Deny requests to paths that should never be publicly reachable.",
			"active": true,
			"conditionGroup": [
				{
					"conditions": [{ "type": "path", "op": "pre", "value": "/.git" }]
				}
			],
			"action": { "mitigate": { "action": "deny" } }
		}
	],
	"ips": []
}
```

That single custom rule is a sample deny on path prefix `/.git`. OWASP managed rules start inactive. `ips` is an empty array.

<ParamField body="--config" type="string" required={false}>
Path written by `init`. Short flag `-c`. Default `firewall.config.json`.
</ParamField>

## Pull the live config

```mermaid
sequenceDiagram
  participant CLI as vwaffle pull
  participant CTX as resolveContext
  participant API as GET /active
  participant OUT as stdout or --output
  CLI->>CTX: VERCEL_TOKEN, --project/--team or env or .vercel/project.json
  CTX-->>CLI: token, projectId, teamId?
  CLI->>API: Authorization Bearer, projectId, optional teamId
  API-->>CLI: { active } or FirewallConfig
  Note over CLI: unwrap live.active when present
  alt --output FILE
    CLI->>OUT: tab-indented JSON + newline
  else no --output
    CLI->>OUT: print JSON
  end
```

<Steps>
<Step title="Set token and project">

```sh
export VERCEL_TOKEN=...
# optional if the directory is already `vercel link`ed
export VERCEL_PROJECT_ID=...
export VERCEL_TEAM_ID=...
```

</Step>
<Step title="Print or write the active config">

<CodeGroup>

```sh title="stdout"
vwaffle pull
vwaffle pull --project prj_... --team team_...
```

```sh title="file"
vwaffle pull --output firewall.config.json
vwaffle pull -o firewall.config.json --project prj_... --team team_...
```

</CodeGroup>

</Step>
<Step title="Confirm the write">

With `--output`:

```text
Wrote active firewall configuration to firewall.config.json.
```

Without `--output`, the tab-indented JSON is the entire stdout payload.

</Step>
</Steps>

<ParamField body="--output" type="string" required={false}>
Destination file for `pull`. Short flag `-o`. Relative paths resolve from `process.cwd()`. Parent directories are not created.
</ParamField>

<ParamField body="--project" type="string" required={false}>
Vercel project ID override. Else `VERCEL_PROJECT_ID`, else `.vercel/project.json` `projectId`.
</ParamField>

<ParamField body="--team" type="string" required={false}>
Vercel team ID override. Else `VERCEL_TEAM_ID`, else `.vercel/project.json` `orgId`. Omitted from the query string when unset.
</ParamField>

:::endpoint GET /v1/security/firewall/config/active Fetch the live firewall config
`getActiveConfig` calls `GET` on `${VERCEL_API_URL ?? "https://api.vercel.com"}/v1/security/firewall/config/active`.

**Query**

| Name | Required | Source |
| --- | --- | --- |
| `projectId` | yes | resolved project |
| `teamId` | no | resolved team, omitted when absent |

**Headers**

| Name | Value |
| --- | --- |
| `Authorization` | `Bearer ${VERCEL_TOKEN}` |
| `Content-Type` | `application/json` |

**Response handling**

If the JSON body has an `active` object, that object is written. Otherwise the whole parsed body is treated as the config. The written file is that unwrapped object, not the outer envelope.

**Errors**

Non-OK responses throw:

```text
Vercel API <status> <statusText>: <body>
```
:::

<Warning>
`pull` does not interpolate `${VAR_NAME}` and does not redact values. A live config that already contains secrets is written in the clear. Prefer placeholders in the desired file after the first pull. `pull --output` overwrites the target with no confirmation and does not create missing parent directories.
</Warning>

## After the file exists

`plan` and `apply` load the desired file with `--config` (default `firewall.config.json`). They interpolate `${VAR_NAME}` from the environment; `pull` does not.

<RequestExample>

```sh
vwaffle plan
vwaffle apply --dry-run
vwaffle apply --yes
```

</RequestExample>

Typical next edits: keep or replace the starter `/.git` deny, turn on `managedRules.owasp`, add `ips` entries, and replace live secret strings with `${VAR_NAME}` placeholders.

## Troubleshooting

| Symptom | Cause | What to do |
| --- | --- | --- |
| `<file> already exists; refusing to overwrite.` | `init` found the target via `access` | Use a new `--config` path, or replace via `pull --output` |
| `VERCEL_TOKEN is required...` | `pull` without a token | Export `VERCEL_TOKEN` |
| `A project is required...` | No `--project`, `VERCEL_PROJECT_ID`, or linked `projectId` | Pass `--project`, set the env var, or run `vercel link` |
| `Vercel API <status> ...` | Firewall API rejected the GET | Check token scope, project, team, and `VERCEL_API_URL` |
| `ENOENT` on `--output` | Parent directory missing | `init --config` creates parents; `pull --output` does not |
| `unknown option ...` | Flag not in the parser | Valid flags: `--config`/`-c`, `--output`/`-o`, `--project`, `--team`, `--yes`, `--dry-run`, `--check`, `--help`/`-h`, `--version`/`-v` |
| `unknown command ...` | First non-flag token is not `init`, `pull`, `plan`, `apply`, `help`, or `version` | Run `vwaffle help` |

<Info>
Override the API host with `VERCEL_API_URL`. The client appends `/v1/security/firewall/config` to that value before `/active`.
</Info>

## Next

<CardGroup>
<Card title="Author firewall rules" href="/author-firewall-rules">
Edit the desired JSON after init or pull.
</Card>
<Card title="Desired vs live config" href="/desired-vs-live-config">
How the local file relates to the active config `pull` writes.
</Card>
<Card title="Preview and apply changes" href="/preview-and-apply-changes">
Diff with `plan`, then PUT with `apply --yes`.
</Card>
<Card title="Project and team context" href="/project-context">
How `resolveContext` picks token, project, and team.
</Card>
</CardGroup>
