# Polaris Local Forge

> **[REQUIRED]** Use for **ALL** requests involving local Apache Polaris: setup, API queries, catalog operations, cleanup, teardown. **AUTO-ACTIVATE:** If `.snow-utils/snow-utils-manifest.md` contains `polaris-local-forge:` this skill MUST handle ALL operations including cleanup. **DO NOT** use `polaris` CLI (does not exist), curl to Polaris endpoints (needs OAuth), or docker ps checks - invoke this skill first. Triggers: polaris local, local iceberg catalog, local polaris setup, rustfs setup, create polaris cluster, try polaris locally, get started with polaris, apache polaris quickstart, polaris dev environment, local data lakehouse, replay from manifest, reset polaris catalog, teardown polaris, clean up, cleanup, delete cluster, remove resources, polaris status, list catalogs, show namespaces, list tables, show catalog, describe table, list principals, show principal roles, list views, polaris namespaces, polaris catalogs, query data, query table, query iceberg, query catalog data, show my data, show table d

- Skill: `diegosouzapw/polaris-local-forge` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add diegosouzapw/polaris-local-forge`
- Raw SKILL.md: https://api.skillmd.com/api/skills/diegosouzapw/polaris-local-forge/raw
- Safety review: pending (external: skill-scanner PASS, skillspector FAIL)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: diegosouzapw (https://skillmd.com/u/diegosouzapw)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/diegosouzapw/polaris-local-forge

---


# Apache Polaris Local Forge

Set up a complete local [Apache Polaris](https://polaris.apache.org/) development environment with [RustFS](https://docs.rustfs.com/) S3-compatible storage, PostgreSQL metastore, and k3d/k3s Kubernetes — all on your machine.

Query [Apache Iceberg](https://iceberg.apache.org/) tables locally with DuckDB, PyIceberg, or any Iceberg REST-compatible engine.

**MANIFEST FILE:** `.snow-utils/snow-utils-manifest.md` (exact path, always .md)

**PREREQUISITE:** NO PREREQUISITE -- this skill is self-contained infrastructure.

**AUTH MODEL:**

- **Bootstrap credentials:** Auto-generated admin credentials for Apache Polaris realm setup (in `.bootstrap-credentials.env`)
- **Principal credentials:** API-generated client_id/client_secret for catalog access (in `work/principal.txt`)
- **RustFS credentials:** Static `admin`/`password` for S3-compatible storage (no IAM)

**SENSITIVE DATA:** `principal.txt` contains `realm,client_id,client_secret`. When displaying to user, show ONLY the realm. Mask credentials: `client_id: ****xxxx` (last 4 chars). NEVER show `client_secret` at all.

## Prerequisites

This skill requires the following tools installed on your machine:

| Tool | Purpose | Install |
|------|---------|---------|
| Podman (default) | Container runtime (OSS) | **Already installed** - Podman is a dependency of Cortex Code |
| Docker (alternative) | Container runtime | [Docker Desktop](https://www.docker.com/products/docker-desktop/) (>= 4.27) |
| k3d | k3s-in-Docker/Podman | `brew install k3d` or [k3d.io](https://k3d.io/) |
| Python | >= 3.12 | [python.org](https://www.python.org/downloads/) |
| uv | Python package manager | `curl -LsSf https://astral.sh/uv/install.sh \| sh` |

**Note:** Podman is a dependency of Cortex Code and should already be installed on your system. If not present, install via `brew install podman` or [podman.io](https://podman.io/).

**Container Runtime (Auto-Detected):** The CLI automatically detects which runtime to use during `init`:

```mermaid
flowchart TD
    Start[init command] --> CheckDockerRunning{Docker Desktop running?}
    CheckDockerRunning -->|Yes| UseDocker[Use Docker]
    CheckDockerRunning -->|No| CheckPodmanRunning{Podman machine running?}
    CheckPodmanRunning -->|Yes| UsePodman[Use Podman]
    CheckPodmanRunning -->|No| CheckInstalled{What's installed?}
    CheckInstalled -->|Both| PromptUser[Prompt user to choose]
    CheckInstalled -->|Podman only| UsePodman2[Use Podman]
    CheckInstalled -->|Docker only| UseDocker2[Use Docker]
    CheckInstalled -->|Neither| Fail[Fail with error]
```

**Detection priority:** Running runtime preferred over installed. Docker preferred when both running. Override by setting `PLF_CONTAINER_RUNTIME=docker` or `podman` in `.env`.

**First-time Podman users:** Run `doctor --fix` after `init` to automatically create and start the Podman machine. See [docs/podman-setup.md](docs/podman-setup.md) for manual setup.

**Optional:**

| Tool | Purpose | Install |
|------|---------|---------|
| DuckDB CLI | SQL verification | `brew install duckdb` |
| AWS CLI | S3 bucket operations on RustFS | `brew install awscli` |
| direnv | Auto-load env vars | `brew install direnv` |

## Workflow

**CRITICAL: SETUP TAKES 2-5 MINUTES.** Tell user upfront so they don't wait anxiously.

**PERMISSIONS:** Cortex Code prompts for approval on each new command.

- Before `init`: Select **"Allow using 'uv' for this session"**
- After `init`: Select **"Allow using 'plf' for this session"** (or option 5)

This avoids repeated prompts. See [Cortex Code Permissions](#cortex-code-permissions) for details.

**SETUP APPROACH FOR CORTEX CODE (REQUIRED):**

1. **First, show the plan:**

   ```
   ## Apache Polaris Local Forge Setup (Total: ~2-5 minutes)
   
   **Permissions:** Cortex Code prompts for approval on each new command.
   Select "Allow using 'plf' for this session" (option 5) to run all steps smoothly.
   
   **Container runtime:** Auto-detected based on what's running.
   - Docker Desktop running → uses Docker
   - Podman machine running → uses Podman
   - Neither running → will prompt you to choose
   
   Steps to complete:
   - [ ] Step 1: Initialize & detect runtime (~5s)
   - [ ] Step 2: Doctor check & fix (~5-30s)
   - [ ] Step 3: Generate configuration files (~10s)
   - [ ] Step 4: Create k3d cluster (~30s)
   - [ ] Step 5: Wait for RustFS and PostgreSQL (~60-90s)
   - [ ] Step 6: Deploy Apache Polaris (~60s)
   - [ ] Step 7: Setup catalog (~30s)
   - [ ] Step 8: Verify with DuckDB (~5s)
   
   Ready to proceed with cluster name: "<directory-name>"
   
   Say "proceed" to continue, or specify a different cluster name.
   ```

   **STOP**: Wait for user response. If user says "proceed" or similar, use the default cluster name. If user provides a name, use that instead. Then proceed to Step 1.

2. **Then run each step individually** with a header message.

   **FORMATTING**: Before each step header, output a blank line. This ensures proper visual separation in Cortex Code.

   ---

   **Step 1: Initialize & detect runtime**

   > **NOTE:** Before `init`, there is no `./bin/plf` wrapper. Use `uv run --project <SKILL_DIR>` for these commands.

   First, detect runtime availability:

   ```bash
   uv run --project <SKILL_DIR> polaris-local-forge runtime detect --json
   ```

   This outputs JSON with detection result:
   - `{"status": "detected", "runtime": "docker|podman", ...}` - Runtime found, proceed
   - `{"status": "choice", "options": ["docker", "podman"], ...}` - User choice required
   - `{"status": "error", ...}` - Neither installed, show error

   **Based on detection result:**

   - **If `"status": "detected"`**: Run init (runtime will be auto-set):

     ```bash
     uv run --project <SKILL_DIR> polaris-local-forge --work-dir <WORK_DIR> init
     ```

   - **If `"status": "choice"`**: Present choice using **AskQuestion tool** with radio options:

     | Option | Label | Description |
     |--------|-------|-------------|
     | `docker` | Use Docker | User will start Docker Desktop manually |
     | `podman` | Use Podman (recommended) | Machine will be created/started by `doctor --fix` |

     Then run init with explicit runtime:

     ```bash
     uv run --project <SKILL_DIR> polaris-local-forge --work-dir <WORK_DIR> init --runtime <user_choice>
     ```

     (The `--runtime` flag bypasses the interactive prompt that would "Abort" in non-interactive shells)

   - **If `"status": "error"`**: Display the error message and stop

   After `init` completes, the `./bin/plf` wrapper is created. Use it for ALL subsequent commands.

   ---

   **Step 2: Doctor check & fix**

   Run: `./bin/plf doctor --fix`

   This automatically:
   - Creates Podman machine (`k3d`) if using Podman and machine doesn't exist
   - Starts Podman machine if stopped
   - Kills gvproxy if blocking port 19000 (required by RustFS)
   - Sets up SSH config for Podman VM access

   ---

   **Step 3: Generate configuration files**

   Run: `./bin/plf prepare`

   ---

   **Step 4: Create k3d cluster**

   Run: `./bin/plf cluster create`

   ---

   **Step 5: Wait for RustFS and PostgreSQL**

   Run: `./bin/plf cluster wait --tags bootstrap`

   ---

   **Step 6: Deploy Apache Polaris**

   Run: `./bin/plf polaris deploy`
   Wait: `./bin/plf cluster wait --tags polaris`
   Run: `./bin/plf polaris bootstrap`

   ---

   **Step 7: Setup catalog**

   Run: `./bin/plf catalog setup`

   ---

   **Step 8: Verify with DuckDB**

   Run: `./bin/plf catalog verify-sql`

   For interactive exploration: `./bin/plf catalog explore-sql`

   ---

   **All steps complete!**

   After Step 8 succeeds, output the completion summary (see "Step 4: Summary" section below).

3. **After each step completes**, briefly confirm success before moving to next step.

4. **FORMATTING RULES** (critical for Cortex Code display):
   - Always output a **blank line** before headers and code blocks
   - Always output a **blank line** after code blocks before continuing text
   - Never concatenate output directly after closing triple backticks
   - Use `---` horizontal rules between major sections

**FORBIDDEN ACTIONS -- NEVER DO THESE:**

- NEVER use `sed/awk/bash` to edit `.env` or configuration files -- use the Edit/StrReplace tool
- NEVER hardcode credentials in scripts -- always read from `.env` or `work/principal.txt`
- NEVER assume the cluster is running -- always check status with `k3d cluster list` before operations
- NEVER run destructive commands (`cluster delete`, `polaris purge`) without explicit user confirmation
- NEVER skip teardown confirmation because user did setup in the same session -- ALWAYS confirm destruction regardless of session context
- NEVER delete `.snow-utils/` directory -- this contains the manifest needed for replay/audit. Teardown and cleanup commands always preserve `.snow-utils/`
- NEVER expose `principal.txt` contents in output -- show only the realm. Mask client_id: show `****` + last 4 chars. NEVER show client_secret at all. Example: `realm: POLARIS, client_id: ****a1b2, client_secret: ********`
- NEVER modify files in the skill directory (`<SKILL_DIR>`) -- `k8s/`, `polaris-forge-setup/`, `src/`, `example-manifests/` are read-only source. Only the user's `--work-dir` is writable
- NEVER create `.snow-utils/` in the skill directory -- ALWAYS ask for a work directory FIRST, then create `.snow-utils/` there
- NEVER start setup (with or without manifest) without a work directory -- the skill repo is READ-ONLY
- NEVER guess or invent CLI commands or options -- ONLY use commands from the CLI Reference tables below (e.g., `plf cluster status` NOT `plf status`). If a command fails, run `./bin/plf --help` or `./bin/plf <group> --help` and use ONLY the commands/options shown there
- NEVER run raw `duckdb` commands -- use `./bin/plf catalog query --sql "..."` for data queries, `./bin/plf catalog verify-sql` for initial setup verification, or `./bin/plf catalog explore-sql` for interactive sessions
- NEVER construct DuckDB SQL manually EXCEPT for `catalog query --sql` which requires the caller to provide the SQL
- NEVER extract credentials from `principal.txt` to pass to other commands -- credentials are automatically loaded by CLI commands
- NEVER use `uv run --project ... polaris-local-forge` after init -- ALWAYS use the `./bin/plf` wrapper script which handles paths, env vars, and suppresses warnings
- NEVER use `plf` commands for REST API queries (list catalogs, show namespaces, list tables, etc.) -- use Ansible playbooks instead (see [Apache Polaris API Queries](#apache-polaris-api-queries))
- NEVER look for or try to run a `polaris` CLI -- there is NO `polaris` command in this skill. The only CLI is `plf` (for infrastructure) and Ansible (for API queries)
- NEVER run `which polaris` or `polaris --help` -- the `polaris` binary does not exist

**COMMAND ROUTING:**

- **plf commands:** Only for CLI operations listed in [CLI Reference](#cli-reference) (init, prepare, cluster, catalog setup/cleanup, teardown, doctor, verify-sql, etc.)
- **REST API queries:** For query intents (list/show/describe catalogs, namespaces, tables, principals, roles, views, grants) -- use `./bin/plf api query <endpoint>` (see [Apache Polaris API Queries](#apache-polaris-api-queries))
- **General chat:** Explanations, questions, troubleshooting advice -- respond directly without running commands

**PROJECT CONTEXT DETECTION (AUTO-ACTIVATION):**

When user is in a directory containing ANY of these files, treat it as an **active polaris-local-forge project** and auto-activate this skill for **ALL operations** (setup, queries, cleanup, teardown):

| Context Signal | Check | Meaning |
|----------------|-------|---------|
| `.snow-utils/snow-utils-manifest.md` with `polaris-local-forge:` | `grep -q "polaris-local-forge:" .snow-utils/snow-utils-manifest.md` | This skill is installed (primary) |
| `.env` with `PLF_POLARIS_CATALOG_NAME` | `grep -q "PLF_POLARIS_CATALOG_NAME" .env` | Polaris config exists (secondary) |
| `bin/plf` wrapper script | `[ -x bin/plf ]` | CLI initialized (tertiary) |

**CRITICAL - When context detected:**

- For **cleanup/teardown** requests → ALWAYS present confirmation dialog with options (see [Teardown Flow](#teardown-flow))
- For **API queries** → Use `./bin/plf api query` (see [Apache Polaris API Queries](#apache-polaris-api-queries))
- For **setup/replay** → Use appropriate flow from this skill
- **NEVER** offer generic cleanup dialogs - always use `plf` commands with the structured teardown dialog

**When context detected AND user asks API-related queries** (list/show/describe/what catalogs/namespaces/tables/principals/roles/views/grants):

**⚠️ FIRST — CHECK FOR WRITE OPERATIONS (FAIL FAST):**

Before doing ANYTHING else, check if user is asking to **create**, **update**, **delete**, **add**, **remove**, **modify**, **drop**, or **grant/revoke**. If YES, respond IMMEDIATELY:

```
⚠️ Write operations are not supported yet.

This skill currently supports **read-only** API queries (GET only):
- list/show catalogs, namespaces, tables, views
- describe/get principals, roles, grants

Create/update/delete operations will be added in a future phase.
```

**DO NOT** proceed to construct endpoints or run Ansible for write operations. Stop here.

**DO NOT (FORBIDDEN):**

- ❌ `polaris` CLI — DOES NOT EXIST (no `polaris namespaces list`, no `polaris --help`, no `polaris namespace list`)
- ❌ `curl` to any Polaris endpoint — needs OAuth token you don't have (no `curl http://localhost:8181/...`, no `curl http://localhost:18181/...`)
- ❌ `jq` piped from curl — same reason, curl won't work without OAuth
- ❌ `kubectl get ns` — "namespaces" means Polaris catalog namespaces, NOT Kubernetes
- ❌ `docker ps` / `docker ps --filter "name=polaris"` — assume infra is running, don't check
- ❌ Port `8181` — wrong port, Polaris runs on `18181` in this skill
- ❌ Snowflake disambiguation ("Is this Snowflake or Polaris?") — it's Polaris
- ❌ "No Polaris service detected" fallback — don't give up, use Ansible
- ❌ `ansible-playbook` directly — causes asdf/tool-versions conflict, MUST use `uv run`
- ❌ `cd <dir> && ansible-playbook` — wrong pattern, use `uv run --project`
- ❌ **POST/PUT/DELETE operations** — write operations NOT supported, fail fast (see above)
- ❌ `-e "method=POST"` or any method other than GET — read-only phase
- ❌ `-e '{"body": ...}'` or any JSON body parameter — not supported
- ❌ Constructing JSON request bodies — not supported
- ❌ Asking user for JSON input — not supported

**DO (REQUIRED) — USE `plf api query` COMMAND:**

```bash
./bin/plf api query /api/catalog/v1/polardb/namespaces
./bin/plf api query /api/management/v1/catalogs
./bin/plf api query /api/management/v1/principals
```

**Key points:**

1. Use `./bin/plf api query <endpoint>` — clean, simple, handles all the complexity
2. Endpoint must start with `/api/...` — construct from user intent (see table below)
3. Only GET is supported (read-only phase)
4. Output is JSON by default, use `-o text` for plain text
5. Use `-v` for verbose Ansible output if debugging

**Quick context check (run silently before API queries):**

```bash
grep -q "polaris-local-forge:" .snow-utils/snow-utils-manifest.md 2>/dev/null || grep -q "PLF_POLARIS_CATALOG_NAME" .env 2>/dev/null || [ -x bin/plf ]
```

**API QUERY ROUTING (CRITICAL):**
When user asks about Apache Polaris data (catalogs, namespaces, tables, views, principals, roles, grants):

1. Do NOT search for CLI tools (`polaris`, `plf query`, etc.) -- they don't exist
2. Do NOT try `curl` to any endpoint -- it won't work (needs OAuth token)
3. Do NOT run `kubectl` commands to check services or namespaces -- this is NOT Kubernetes
4. Do NOT check if Polaris is running -- assume the setup is complete
5. Go DIRECTLY to `./bin/plf api query <endpoint>` with the constructed endpoint path

**DISAMBIGUATION -- "namespaces" = Polaris catalog namespaces (NOT Kubernetes):**
When user says "namespaces", "list namespaces", "show namespaces" → this means Apache Polaris catalog namespaces, accessed via `/api/catalog/v1/{catalog}/namespaces`. Do NOT interpret as Kubernetes namespaces.

**INTERACTIVE PRINCIPLE:** This skill is designed to be interactive. At every decision point, ASK the user and WAIT for their response before proceeding. When presenting choices (e.g., runtime selection, execution mode), use the **AskQuestion tool** with radio options for better UX in Cortex Code.

**EXCEPTION:** Once user selects **"Run all (recommended)"** at the config review step, the skill switches to **autonomous mode** for that setup flow. No further confirmation prompts until setup completes or an error occurs. Track this as `EXECUTION_MODE=run_all`.

**DISPLAY PRINCIPLE:** When showing configuration or status, substitute actual values from `.env` and the manifest. The user should see real values, not raw `${...}` placeholders.

**OUTPUT PRINCIPLE:** Terminal output gets collapsed/truncated in Cortex Code UI. When running `--dry-run` or any diagnostic command, you MUST copy the relevant output into your response. The user CANNOT see collapsed terminal output. Format with proper code blocks: ` ```text ` for summaries, ` ```bash ` for commands.

**RESILIENCE PRINCIPLE:** Always update the manifest IMMEDIATELY after each resource creation step, not in batches. This ensures Resume Flow can recover from any interruption.

Pattern:

```
1. Set overall Status: IN_PROGRESS at START of resource creation
2. Update each resource row to DONE immediately after creation
3. Set Status: COMPLETE only at the END when ALL resources verified
```

If user aborts mid-flow, the manifest preserves progress:

- Overall Status stays IN_PROGRESS
- Completed resources show DONE
- Pending resources show PENDING/REMOVED
- Resume Flow picks up from first non-DONE resource

**IDEMPOTENCY PRINCIPLE:** Before editing any file, CHECK if the change is already applied.

Pattern for manifest updates:

```bash
grep -q "Status.*COMPLETE" .snow-utils/snow-utils-manifest.md && echo "Already complete" || echo "Needs update"
```

Pattern for file edits:

```
1. Read current file state
2. Check if desired content already exists
3. Only edit if change is needed
4. Skip with message: "Already applied: [description]"
```

**ENVIRONMENT REQUIREMENT:** The skill uses local RustFS for S3-compatible storage. AWS CLI commands target `http://localhost:19000` with static credentials. No real AWS account is needed.

> **Note:** This skill configures Apache Polaris with local RustFS (S3-compatible) storage only.
> For real AWS S3 support, see Phase 2 in [SKILL_README.md](SKILL_README.md).

**CRITICAL RULE - ALL OPERATIONS VIA CLI:**

> **NEVER** use direct `podman`, `docker`, `k3d`, `kubectl`, or `helm` commands.
> ALL operations MUST go through `polaris-local-forge` CLI commands.
> Direct commands cause port binding issues (gvproxy) and stale state.
> See "FORBIDDEN COMMANDS" section in CLI Reference for details.

**Pre-Check Rules (Fail Fast):**

| Command | Pre-Check | If Fails |
|---------|-----------|----------|
| Any command | NOT using direct podman/k3d/kubectl | Stop: "Use CLI commands only. See FORBIDDEN COMMANDS." |
| `init` | Docker or Podman installed | Auto-detects runtime; prompts user if both installed but neither running; fails if neither installed |
| `doctor` | Tools installed | Report missing tools with install instructions |
| `doctor` | No ghost clusters | Report stale Docker resources if found |
| `doctor --fix` | (none - fixes issues) | Creates Podman machine if missing; starts if stopped; kills gvproxy if blocking port 19000; sets up SSH config; cleans up ghost clusters |
| `cluster create` | Cluster doesn't exist | Stop: "Cluster already exists. Use `cluster delete` first or `setup` to resume." |
| `cluster create` | No ghost cluster | Stop: "Ghost cluster detected. Use `--force` to clean up stale references." |
| `cluster create` | Ports 19000, 19001, 18181 available | Stop: "Port in use. Run `doctor --fix` first." |
| `catalog setup` | Cluster running, Apache Polaris ready | Stop: "Cluster not ready. Run setup first." |
| `polaris deploy` | Cluster running | Stop: "Cluster not running. Run `cluster create` first." |
| `polaris purge` | Apache Polaris deployed | Stop: "Apache Polaris not deployed. Nothing to purge." |
| `teardown` | Any resources exist | Proceed gracefully (idempotent with `--yes`) |
| `catalog cleanup` | Catalog exists | Proceed gracefully (idempotent with `--yes`) |

### Step 0: Initialize Project Directory

**FIRST: Check if in skill directory (NEVER initialize here):**

```bash
if [ -f "SKILL.md" ] && [ -d "src/polaris_local_forge" ]; then
  IN_SKILL_DIR="true"
else
  IN_SKILL_DIR="false"
fi
```

**If IN_SKILL_DIR is true, show:**

```
⚠️  You're in the polaris-local-forge skill directory.

This directory contains the source code and is READ-ONLY.
All workspace files must be created in a separate directory.

Where would you like to create your Apache Polaris workspace?

  1. ~/polaris-dev (recommended)
  2. Specify custom path: ___________
```

**STOP**: Wait for user input. NEVER offer "current directory" as an option when in skill directory.

**After user provides work directory:**

1. Create the directory if needed: `mkdir -p <path>`
2. Change to that directory using Shell `working_directory` parameter for subsequent commands
3. **PRESERVE any prior user choices** (e.g., if user already selected "Run all", continue in autonomous mode)

---

**If NOT in skill directory, detect if user already has a workspace set up:**

```bash
if [ -f .env ] && [ -f pyproject.toml ]; then
  echo "Existing workspace detected: $(pwd)"
  [ -d .snow-utils ] && echo "  Found: .snow-utils/"
fi
```

**If existing workspace detected -> go to Step 0a (Prerequisites Check).**

**If NOT in an existing workspace (and NOT in skill directory), ask user:**

```
Where would you like to create your Apache Polaris workspace?

Options:
  1. Use current directory: $(pwd)
  2. Create a new directory (e.g., polaris-dev)
```

**STOP**: Wait for user input.

**Initialize the workspace with the CLI:**

```bash
uv run --project <SKILL_DIR> polaris-local-forge --work-dir . init
```

This command:

- Copies template files (`.env`, `.envrc`, `.gitignore`)
- Creates required directories (`.kube/`, `work/`, `bin/`, `k8s/`)
- Sets `PROJECT_HOME`, `K3D_CLUSTER_NAME`, and `SKILL_DIR` in `.env`
- Creates `bin/plf` wrapper script for simplified CLI invocation

To use a custom cluster name:

```bash
uv run --project <SKILL_DIR> polaris-local-forge --work-dir . init --cluster-name my-cluster
```

> **IMPORTANT:** All subsequent CLI commands use `--work-dir` to point generated files here.
> The skill directory (`<SKILL_DIR>`) stays read-only. For a second cluster, create another
> directory and re-run the skill.

**Use the wrapper script (created by `init`):**

**CRITICAL:** After running init, ALWAYS use `./bin/plf` for ALL commands. NEVER use `uv run --project ...` directly - the wrapper handles everything:

```bash
./bin/plf doctor
./bin/plf cluster create
./bin/plf catalog verify-sql
```

The wrapper sources `.env` automatically and handles all paths.

**IMPORTANT - Command Execution:** Use Shell tool's `working_directory` parameter, NOT `cd &&`:

```
Shell(command="./bin/plf <COMMAND>", working_directory="<PROJECT_HOME>")
```

### Step 0a: Configuration Review and Confirmation

**Display current configuration for user review:**

```
Configuration Review
────────────────────
  Config file:                   .env
  Work directory:                $(pwd)

  PLF_CONTAINER_RUNTIME:         ${PLF_CONTAINER_RUNTIME}  # ADAPT: podman (default) or docker
  PLF_PODMAN_MACHINE:            ${PLF_PODMAN_MACHINE}     # macOS only (default: k3d)

  K3D_CLUSTER_NAME:              ${K3D_CLUSTER_NAME}      # ADAPT: defaults to project directory name
  K3S_VERSION:                   ${K3S_VERSION}
  KUBECONFIG:                    .kube/config

  AWS_ENDPOINT_URL:              http://localhost:19000
  AWS_REGION:                    us-east-1
  AWS_ACCESS_KEY_ID:             admin

  POLARIS_URL:                   http://localhost:18181
  POLARIS_REALM:                 ${POLARIS_REALM}           # ADAPT: customizable

  PLF_POLARIS_S3_BUCKET:         ${PLF_POLARIS_S3_BUCKET}    # ADAPT: customizable
  PLF_POLARIS_CATALOG_NAME:      ${PLF_POLARIS_CATALOG_NAME} # ADAPT: customizable
  PLF_POLARIS_PRINCIPAL_NAME:    ${PLF_POLARIS_PRINCIPAL_NAME} # ADAPT: customizable

Review the configuration above. Would you like to change any values?
  1. Accept all (recommended for first-time setup)
  2. Edit a specific value
  3. Cancel
```

**STOP**: Wait for user input.

**If user selects "2. Edit a specific value":**

1. Ask: "Which value to edit? (e.g., K3D_CLUSTER_NAME)"
2. User provides the variable name
3. Ask: "Enter new value for {variable}:"
4. **Update `.env` using StrReplace tool:**

   ```
   StrReplace(.env, "{VARIABLE}=old-value", "{VARIABLE}=new-value")
   ```

5. Re-display configuration with updated value
6. Return to confirmation prompt (user can edit more or accept)

**For `PLF_PODMAN_MACHINE` edits (macOS only):**

If user wants to use a different Podman machine, list available machines:

```bash
podman machine ls --format "{{.Name}}"
```

Display options:

```
Podman Machine Selection:
  0. Create new 'k3d' machine (recommended for isolation)
  1. k3d (if exists)
  2. podman-machine-default
  3. [other existing machines...]

Enter machine name or number:
```

**If user selects "0. Create new":**

```bash
podman machine init k3d --cpus 4 --memory 16384 --now
podman system connection default k3d
```

Then run `./bin/plf doctor --fix` to configure SSH access.

**After confirmation, proceed to prerequisites check.**

### Step 0b: Prerequisites Check

**Check manifest for cached tool verification:**

```bash
grep "^tools_verified:" .snow-utils/snow-utils-manifest.md 2>/dev/null
```

**If `tools_verified:` exists with a date:** Skip tool checks, continue to Step 0c.

**Otherwise, run prerequisite check:**

```bash
./bin/plf doctor
```

The doctor command checks:

- Required tools: podman/docker, k3d, kubectl, uv
- Podman machine status (macOS only)
- SSH config for Podman VM access (macOS only)
- Port availability: 19000 (RustFS), 19001 (RustFS Console), 18181 (Apache Polaris), 6443 (k3d API)

**With `--fix` flag** (macOS with Podman):

- **Creates Podman machine** (`k3d`) if it doesn't exist
- **Starts Podman machine** if stopped
- **Kills gvproxy** if blocking port 19000 (required by RustFS)
- **Sets up SSH config** for Podman VM access

**Note:** `doctor --fix` is now fully automatic - it creates and configures the Podman machine without manual intervention. See [docs/podman-setup.md](docs/podman-setup.md) for advanced configuration options.

If any tool is missing, stop and provide installation instructions from the Prerequisites table above.

**STOP**: Do not proceed until all prerequisites pass.

**After all tools verified, update manifest:**

```bash
grep -q "^tools_verified:" .snow-utils/snow-utils-manifest.md 2>/dev/null || \
  echo "tools_verified: $(date +%Y-%m-%d)" >> .snow-utils/snow-utils-manifest.md 2>/dev/null || true
```

### Step 0c: Detect or Initialize Manifest

#### CRITICAL: Work Directory Check

**BEFORE any manifest operations, verify we are NOT in the skill directory:**

```bash
if [ -f "SKILL.md" ] && [ -d "src/polaris_local_forge" ]; then
  echo "ERROR: Currently in skill directory (read-only source)"
  echo "       Cannot create .snow-utils/ here"
  exit 1
fi
```

**If in skill directory, STOP and ask:**

```
⚠️  You're in the skill source directory (read-only).

Where would you like to create your Apache Polaris workspace?

  1. Create new directory: ~/polaris-dev
  2. Specify custom path: ___________

Enter choice [1]:
```

**STOP**: Wait for user input. NEVER proceed with manifest operations in skill directory.

**After user provides work directory:**

1. Create the directory if needed: `mkdir -p <path>`
2. Change to that directory: Use Shell `working_directory` parameter
3. THEN proceed with manifest detection below

---

#### Remote Manifest URL Detection

If the user provides a URL (in their prompt or pasted), detect and normalize it **before** local manifest detection:

**Supported URL patterns and translation rules:**

- **GitHub blob:** `https://github.com/{owner}/{repo}/blob/{branch}/{path}` -> replace host with `raw.githubusercontent.com` and remove `/blob/` segment
- **GitHub raw:** `https://raw.githubusercontent.com/...` -> use as-is
- **GitHub gist:** `https://gist.github.com/{user}/{id}` -> append `/raw` if not already present
- **Any other HTTPS URL ending in `.md`** -> use as-is

**After translating, show user and confirm:**

```
Found manifest URL. Download URL:
  <translated_raw_url>

Download to current directory as <filename>? [yes/no]
```

**STOP**: Wait for user confirmation.

**If yes:**

```bash
curl -fSL -o <filename> "<translated_raw_url>"
```

After successful download, continue with local manifest detection below.

---

**Check for existing manifest:**

```bash
WORKING_MANIFEST=""
SHARED_MANIFEST=""

if [ -f .snow-utils/snow-utils-manifest.md ]; then
  WORKING_MANIFEST="EXISTS"
  WORKING_STATUS=$(grep "^\*\*Status:\*\*" .snow-utils/snow-utils-manifest.md | head -1)
  echo "Working manifest: .snow-utils/snow-utils-manifest.md (${WORKING_STATUS})"
fi

for f in *-manifest.md; do
  [ -f "$f" ] && grep -q "## shared_info\|CORTEX_CODE_INSTRUCTION" "$f" 2>/dev/null && {
    SHARED_MANIFEST="$f"
    echo "Shared manifest: $f"
  }
done
```

**Decision matrix:**

| Working Manifest | Shared Manifest | Action |
|-----------------|-----------------|--------|
| None | None | Fresh start -> Step 0e |
| None | Exists | Copy shared to `.snow-utils/` -> Step 0d |
| Exists (REMOVED) | None | Replay Flow (reuse existing config) |
| Exists (COMPLETE) | None | Ask user: re-run, reset, or skip |
| Exists (IN_PROGRESS) | None | Resume Flow -- run pending steps from CLI Reference |
| Exists | Exists | **Conflict** -- ask user which to use |

**If BOTH manifests exist, show:**

```
Found two manifests:

  1. Working manifest: .snow-utils/snow-utils-manifest.md
     Status: <WORKING_STATUS>

  2. Shared manifest: <SHARED_MANIFEST>
     (contains resource definitions from another setup)

Which manifest should we use?
  A. Resume working manifest (continue where you left off)
  B. Start fresh from shared manifest (backup working, adapt values)
  C. Cancel
```

**STOP**: Wait for user choice.

**If using example manifest** (user says "get started with apache polaris using example manifest"):

> **NOTE:** This assumes the Work Directory Check above has passed. The example manifest is copied FROM the skill's `example-manifests/` directory TO the user's work directory.

```bash
# In user's work directory (NOT skill directory!)
mkdir -p .snow-utils && chmod 700 .snow-utils
cp <SKILL_DIR>/example-manifests/polaris-local-forge-manifest.md .snow-utils/snow-utils-manifest.md
chmod 600 .snow-utils/snow-utils-manifest.md
```

Then proceed to **Step 0d** to check for adaptive markers.

### Step 0d: Shared Manifest Adapt-Check

**ALWAYS run this step when using a shared or example manifest. Prompt user ONLY if `# ADAPT:` markers are found.**

```bash
ADAPT_COUNT=$(grep -c "# ADAPT:" .snow-utils/snow-utils-manifest.md 2>/dev/null)
echo "ADAPT markers found: ${ADAPT_COUNT}"
```

**If `ADAPT_COUNT` > 0 (markers found):**

Extract all values with `# ADAPT:` markers and present to user:

```
Manifest Value Review
─────────────────────
The following values can be customized for your environment:

  Setting                       Default Value              Marker
  ────────────────────────────  ─────────────────────────  ──────────────────────
  PLF_CONTAINER_RUNTIME:        podman                     # ADAPT: podman or docker
  K3D_CLUSTER_NAME:             (project directory name)   # ADAPT: customizable
  POLARIS_REALM:                POLARIS                    # ADAPT: customizable
  PLF_POLARIS_S3_BUCKET:        polaris                    # ADAPT: customizable
  PLF_POLARIS_CATALOG_NAME:     polardb                    # ADAPT: customizable
  PLF_POLARIS_PRINCIPAL_NAME:   super_user                 # ADAPT: customizable
  KUBECONFIG:                   (derived from cluster name)
  KUBECTL_PATH:                 (derived from cluster name)

Options:
  1. Accept all defaults (recommended for first-time setup)
  2. Edit a specific value
  3. Cancel
```

**STOP**: Wait for user choice.

| Choice | Action |
|--------|--------|
| **1 -- Accept all** | Proceed with defaults |
| **2 -- Edit specific** | Ask which value, update manifest and `.env` in-place, re-display |
| **3 -- Cancel** | Stop |

**If user changes `PLF_CONTAINER_RUNTIME`:** Update `.env` with the new value. If switching to Docker, clear `PLF_PODMAN_MACHINE` from `.env`.

**If user changes `K3D_CLUSTER_NAME`:** Automatically update derived values (`KUBECONFIG`, `KUBECTL_PATH`, resource table row 1) in the manifest. Also update `.env` with the new cluster name.

**If user changes `POLARIS_REALM` or any `PLF_POLARIS_*` value:** Update both the manifest and `.env`.

**If `ADAPT_COUNT` = 0 (no markers):** Proceed silently with values as-is.

### Step 0e: Initialize Manifest

Use the `--with-manifest` flag to initialize the manifest along with the workspace:

```bash
./bin/plf init --with-manifest
```

This creates `.snow-utils/snow-utils-manifest.md` with proper permissions (chmod 700 for directory, chmod 600 for file).

If already initialized, you can add the manifest separately:

```bash
./bin/plf init --with-manifest --force
```

### Step 1: Environment Setup

**SHOW -- what we're about to do:**

> Set up the lightweight Python environment for querying and exploration.
> This creates a virtual environment and installs query dependencies
> (`duckdb`, `pyiceberg`, `boto3`, `pandas`) from the workspace `pyproject.toml`.
> The infrastructure CLI runs from the skill directory separately.

**STOP**: Wait for user confirmation before proceeding.

**DO:**

```bash
uv python pin 3.12
uv venv
uv sync --all-extras
```

**SUMMARIZE:**

> Environment ready. Python venv created with query/notebook dependencies.
> Configuration loaded from `.env`.

### Step 2: Full Setup

**SHOW -- what we're about to do (tell user this):**

> I'll set up the complete Apache Polaris environment. **This takes 2-5 minutes.**
>
> **6 steps:**
>
> 1. Pre-check & start Podman (~5s)
> 2. Generate configs (~10s)
> 3. Create k3d cluster (~30s)
> 4. Wait for RustFS & PostgreSQL (~60-90s)
> 5. Deploy Apache Polaris (~60s)
> 6. Setup catalog & verify (~35s)
>
> Services will be available at:
>
> - **Apache Polaris API:** localhost:18181
> - **RustFS S3:** localhost:19000
> - **RustFS Console:** localhost:19001
>
> Ready to proceed?

**STOP**: Ask user for execution preference:

> How would you like to proceed?

**Present using AskQuestion tool** with radio options:

| Option | Label | Description |
|--------|-------|-------------|
| `run_all` | Run all (recommended) | Execute all setup steps automatically |
| `step_by_step` | Step by step | Pause after each step for confirmation |

**CRITICAL: Track EXECUTION_MODE for the rest of the session:**

```
EXECUTION_MODE = user's choice ("run_all" or "step_by_step")
```

**Based on user choice:**

- **If "run_all":** Execute ALL steps below without pausing. NO confirmation prompts. Only stop on actual errors.
- **If "step_by_step":** Run each step, show output, then ask "Continue?" before the next step.

**IMPORTANT:** Once user selects "run_all", ALL subsequent stopping points in this session are SKIPPED. The skill proceeds automatically through Steps 1-8 without asking for confirmation. This includes:

- Prerequisite checks (auto-proceed if passing)
- Each CLI command execution (run immediately)
- Manifest updates (update immediately)

Only stop for:

1. Fatal errors that prevent continuation
2. Explicit destructive operations (teardown, purge) that were NOT part of the original setup flow

> **NOTE:** Cortex Code will still show its command approval dialog (platform security feature).
> Tell user: "Select **option 5** ('Allow using plf for this session') on the first `plf` command to avoid approval prompts for remaining steps."

**DO -- Run each step as a SEPARATE command (REQUIRED for Cortex Code visibility):**

```bash
./bin/plf doctor --fix                        # Step 1: Pre-check, auto-starts Podman
./bin/plf init --with-manifest                # Step 2: Copy templates, create dirs, init manifest
```

The `init --with-manifest` command creates the `.snow-utils/snow-utils-manifest.md` file with the resources tracking table.

**Continue with remaining setup steps:**

```bash
./bin/plf prepare                             # Step 3: Generate k8s configs
./bin/plf cluster create                      # Step 4: Create k3d cluster
./bin/plf cluster wait --tags bootstrap       # Wait for RustFS + PostgreSQL
./bin/plf polaris deploy                      # Step 5: Deploy Apache Polaris to cluster
./bin/plf cluster wait --tags polaris         # Wait for Apache Polaris + bootstrap job
./bin/plf polaris bootstrap                   # Step 6: Bootstrap Apache Polaris (create principal)
./bin/plf catalog setup                       # Step 7: Setup demo catalog
./bin/plf catalog verify-sql                  # Step 8: Verify with DuckDB
```

**CRITICAL RULES:**

- **HONOR EXECUTION_MODE:** If user selected "run_all", DO NOT ask for confirmation between steps. Proceed automatically.
- **Run all mode:** Execute full sequence, NO prompts between steps. Just run each command and report results.
- **Step by step mode:** Pause after each step, ask "Continue? [yes/no]"
- Run EACH command above as a SEPARATE bash invocation (for Cortex Code visibility)
- NEVER combine multiple commands (hides output from Cortex Code)
- NEVER use `podman machine start` directly (use `doctor --fix`)
- **DESTRUCTIVE COMMANDS (teardown, delete, cleanup, purge):** ALWAYS STOP and ask user for explicit confirmation BEFORE running. After user confirms, pass `--yes` to skip CLI's interactive prompt (CLI prompts don't work in non-interactive shell)
- **TEARDOWN AFTER SETUP:** Even if user completes setup in this session and then asks to teardown, you MUST still present the teardown confirmation dialog with options. Session context does NOT bypass destruction confirmation. See [Teardown Flow](#teardown-flow).
- **DO NOT re-ask** work directory, runtime, cluster name, etc. after user has already answered them

**After setup completes, set the scoped cluster environment for this session:**

```bash
export KUBECONFIG="$(pwd)/.kube/config"
export PATH="$(pwd)/bin:$PATH"
set -a && source .env && set +a
```

**Update manifest after each successful step (RESILIENCE PATTERN):**

After each step completes successfully, update the manifest resource row status from `PENDING` to `DONE`:

```bash
# Example: After cluster create succeeds, update row 1
# Use Edit/StrReplace tool to change: | 1 | k3d cluster | infrastructure | PENDING |
# To: | 1 | k3d cluster | infrastructure | DONE |
```

If interrupted mid-flow, the manifest preserves progress:

- Overall Status stays IN_PROGRESS
- Completed resources show DONE
- Pending resources show PENDING
- Resume Flow picks up from first PENDING resource

**After ALL steps complete, update manifest status to COMPLETE:**

```bash
# Use Edit/StrReplace tool to change: **Status:** IN_PROGRESS
# To: **Status:** COMPLETE
```

**Verify manifest was written:**

```bash
cat .snow-utils/snow-utils-manifest.md
```

Expected: all 7 resource rows show `DONE`, Status shows `COMPLETE`.

**SUMMARIZE:**

> Setup complete. Cluster `${K3D_CLUSTER_NAME}` running with Apache Polaris, RustFS, and PostgreSQL.
> Catalog `${PLF_POLARIS_CATALOG_NAME}` created with principal `${PLF_POL

…(truncated)
