---
name: setup-tundri-repository
description: Scaffold a client's repository with tundri/Permifrost configuration for Snowflake access control. Use when setting up a new client repository with permifrost.yml, requirements.txt, CI/CD workflows, and access control configuration.
disable-model-invocation: true
---

# Set Up a Tundri-Permifrost Repository

Scaffold a client's existing repository with tundri/Permifrost configuration for Snowflake access control. Use this skill when onboarding a new client that needs Snowflake infrastructure.

## Context

Each client has their own repository for data infrastructure. This skill adds the Permifrost configuration files to that repository:

- `permifrost.yml` — the declarative Snowflake configuration (databases, warehouses, roles, users)
- `requirements.txt` — Python dependencies (tundri)
- `.env.example` — template for local environment variables
- `.gitignore` — excludes `.env` and `.venv/`
- `README.md` — client-facing usage guide
- `.github/workflows/` — CI/CD for dry runs on PRs and production runs on merge

All templates are in the `assets/` folder of this skill.

## Prerequisites

- **You must be running from the client's repository** where `permifrost.yml` should live. Repositories are in different organizations for different clients — do not create a new repo.
- Knowledge of which service users the client will need (default: `dbt`; alternatives: `dbt_cloud`/`dbt_cloud_dev`, `airflow`, `metabase`, `airbyte`)

## Steps

**Checklist:**

- [ ] 1. Copy and configure `permifrost.yml`
- [ ] 2. Copy `requirements.txt` and pin tundri version
- [ ] 3. Copy `.gitignore`
- [ ] 4. Copy `.env.example`
- [ ] 5. Copy the client `README.md`
- [ ] 6. Copy GitHub Actions workflows
- [ ] 7. Add CLAUDE.md
- [ ] 8. Adjust permifrost.yml for the client's initial users
- [ ] 9. Store human user credentials in 1Password
- [ ] 10. Run tundri dry run to validate

### 1. Copy and configure `permifrost.yml`

Copy the template from this skill folder:

```bash
cp assets/permifrost_template.yml permifrost.yml
```

The template contains the standard Gemma Snowflake structure:

- **System databases:** `snowflake`, `snowflake_sample_data`, `snowflake_learning_db`, `permifrost`
- **Production databases:** `prod_raw`, `prod_analytics`
- **Development databases:** `dev_<user>_raw`, `dev_<user>_analytics` (one pair per developer)
- **Warehouses:** `load`, `transform`, `report`, `develop`, `admin` (all x-small by default)
- **System roles:** `public`, `accountadmin`, `securityadmin`, `sysadmin`, `useradmin`, `orgadmin`
- **Operations roles:** `loader`, `transformer`, `reporter`, `developer`, `analyst`
- **Role owners:** `ops_role_owner`, `userrole_role_owner`
- **Users:** `snowflake` (system), `permifrost`, example service users, `admin`, and placeholder for human users

Review the template and:

1. **Choose service users.** The default is `dbt` (for dbt Core). If the client uses dbt Cloud instead, replace with `dbt_cloud` and optionally `dbt_cloud_dev`. Remove any service users not needed (e.g. `airbyte`, `metabase`).
2. **Keep at minimum:** `permifrost`, the relevant dbt user, and any data loading user (e.g. `airflow`).
3. **Replace all `<first_initial><last_name>` placeholders** with actual human users.
4. **If dbt Cloud is not used**, remove the `dev_cloud` database and `userrole_dev_cloud` role.

### 2. Copy `requirements.txt` and pin tundri version

```bash
cp assets/requirements_template.txt requirements.txt
```

Check the [latest tundri release](https://github.com/Gemma-Analytics/tundri/releases) and update the version in `requirements.txt`.

### 3. Copy `.gitignore`

```bash
cp assets/.gitignore .gitignore
```

This excludes `.env` (contains secrets) and `.venv/` (local Python environment created by uv).

### 4. Copy `.env.example`

```bash
cp assets/.env.example .env.example
```

This serves as documentation for which environment variables are needed. The actual `.env` file is created during [local development setup](../setup-tundri-local-dev/).

### 5. Copy the client README

```bash
cp assets/CLIENT_README.md README.md
```

This README is written for the client's team and explains how to use permifrost.yml, what each section means, and how the CI/CD works.

### 6. Copy GitHub Actions workflows

```bash
mkdir -p .github/workflows
cp assets/.github/workflows/permifrost-run-dry.yml .github/workflows/
cp assets/.github/workflows/permifrost-run-prod.yml .github/workflows/
cp assets/.github/workflows/permifrost-drop-approval.yml .github/workflows/
```

These workflows:
- **Dry run:** Triggers on PRs that modify `permifrost.yml` — runs `tundri run --dry` to validate changes. Also detects DROP statements and sets a `drop-statement-review` commit status (see [setup-tundri-cicd](../setup-tundri-cicd/) for details).
- **Production run:** Triggers on merge to `main` when `permifrost.yml` is modified — applies changes to Snowflake.
- **DROP approval:** Listens for `/proceed-with-drop-statements` comments on PRs and flips the `drop-statement-review` status to success. Only users with write/admin access can approve.

**Important — admin user naming and `--users-to-skip`:**

Tundri's `--users-to-skip` flag defaults to `["admin", "snowflake", "auto_dba"]`. If your client's `accountadmin`-owned bootstrap user is named exactly `admin` (as the template assumes), no further action is needed.

**However, if it was renamed during account setup** (e.g. `user_admin` to follow a client naming convention), tundri fails at user inspection with `Insufficient privileges to operate on user '<NAME>'` — `securityadmin` doesn't own the renamed user. In that case, pass the flag explicitly in **both workflow files** and in **local dry runs**:

```bash
# Local
uv run tundri run --filepath permifrost.yml --dry --users-to-skip <your_admin_username>
```

```bash
# In permifrost-run-dry.yml
tundri run --filepath permifrost.yml --dry --users-to-skip <your_admin_username>

# In permifrost-run-prod.yml
tundri run --filepath permifrost.yml --users-to-skip <your_admin_username>
```

Note: the flag *replaces* the default skip list, so include `snowflake` if you still want it skipped.

### 7. Add CLAUDE.md

Create a `CLAUDE.md` file in the repository root to instruct Claude Code to always use PRs:

```markdown
# Claude Code Instructions

## Git Workflow

- Never push directly to `main`. Always create a branch and open a pull request.
- The dry run CI check must pass before merging.
```

### 8. Adjust permifrost.yml for the client's initial users

Replace all placeholder values in `permifrost.yml`:

1. Replace `<first_initial><last_name>` with actual usernames for each human user. **Usernames must be valid unquoted Snowflake identifiers** — letters, digits, `_`, `$`, starting with a letter or underscore. **No dots, spaces, or other special characters** (e.g. use `t_rex`, not `t.rex`). Tundri's inspector runs `DESCRIBE USER {user}` without quoting the identifier, so a dotted `first.last` name fails only later, during the CI dry run. See the [add-remove-permifrost-user](../add-remove-permifrost-user/) skill (Username rules) for the detection command and the `ALTER USER "FIRST.LAST" RENAME TO FIRST_LAST;` remedy (which preserves `login_name`, so logins are unchanged).
2. For each human user, add the corresponding database entries, role entries, role owner membership, and user entries. Follow the [add-remove-permifrost-user](../add-remove-permifrost-user/) skill for the exact four-layer pattern (databases, userrole_role_owner, role definition, user entry).
3. **Ask if the first human user(s) should also be granted the `accountadmin` role directly.** This is common for the lead developer or data team lead. If yes, add `- accountadmin` to their `member_of` list and **exclude their userrole from `userrole_role_owner`** (otherwise it creates a role hierarchy cycle).
4. Public keys for service users will be added later. During [setup-tundri-snowflake-account](../setup-tundri-snowflake-account/), you will generate the `permifrost` key pair first. After that, generate key pairs for remaining service users using the [snowflake-generate-keypair](../../snowflake-generate-keypair/) skill and paste each public key into the corresponding user's `rsa_public_key` field.
5. **Recommended:** Also generate key pairs for human users to enable programmatic Snowflake access (e.g. via Python, dbt CLI, or other tools). Use the same [snowflake-generate-keypair](../../snowflake-generate-keypair/) skill and add the public keys to each human user's `rsa_public_key` field in `permifrost.yml`.

### 9. Store human user credentials in 1Password

For each human user added in step 8, create a 1Password entry with their initial credentials. The initial password is the value from the `password` field in `permifrost.yml` (typically `1passwordvault`). Users will be forced to change it on first login (`must_change_password: true`).

```bash
python3 -c "
import json
print(json.dumps({
    'title': '<Client Name> - Snowflake: <username>',
    'category': 'LOGIN',
    'vault': {'id': '<vault_id>'},
    'urls': [{'primary': True, 'href': 'https://<account_identifier>.snowflakecomputing.com'}],
    'fields': [
        {'id': 'username', 'type': 'STRING', 'label': 'username', 'value': '<USERNAME>'},
        {'id': 'password', 'type': 'CONCEALED', 'label': 'password', 'value': '<initial_password>'},
        {'id': 'account_identifier', 'type': 'STRING', 'label': 'account_identifier', 'value': '<ACCOUNT_IDENTIFIER>'},
    ]
}))
" | op item create -
```

Repeat for each human user. The user should update their password in 1Password after their first Snowflake login.

### 10. Run tundri dry run to validate

If the Snowflake account already exists (i.e. [setup-tundri-snowflake-account](../setup-tundri-snowflake-account/) has been completed), validate the configuration with a local dry run.

Set up the local environment:

```bash
uv venv
source .venv/bin/activate
uv pip install -r requirements.txt
```

Create a `.env` file from `.env.example` and fill in the values (see [setup-tundri-local-dev](../setup-tundri-local-dev/) for details). Then run:

```bash
source .venv/bin/activate
set -a && source .env && set +a
tundri run --filepath permifrost.yml --dry
```

**Important:** The venv must be activated before running tundri — it calls `permifrost` as a subprocess and needs it on the PATH.

The dry run connects to Snowflake but does not make any changes. It should complete with "DDL statements to be executed" (tundri phase) and "SQL Commands generated" (permifrost phase) without errors. "Ignored missing" entries are expected for objects that don't exist yet.

## Validation

- [ ] `permifrost.yml` has no `<placeholder>` values remaining (except `rsa_public_key` blocks which will be filled after key generation)
- [ ] `requirements.txt` has the latest tundri version
- [ ] `.gitignore` excludes `.env` and `.venv`
- [ ] `.env.example` is present
- [ ] `README.md` contains the client-facing guide
- [ ] `.github/workflows/` contains dry run, prod run, and DROP approval workflow files
- [ ] No unnecessary service users remain in permifrost.yml
- [ ] All usernames are valid unquoted Snowflake identifiers (no dots/special chars)

## Next steps

After this skill, proceed with:
1. [setup-tundri-snowflake-account](../setup-tundri-snowflake-account/) — create the Snowflake account
2. [snowflake-generate-keypair](../../snowflake-generate-keypair/) — generate keys for the permifrost user and other service users
3. [setup-tundri-local-dev](../setup-tundri-local-dev/) — configure local environment
4. [setup-tundri-cicd](../setup-tundri-cicd/) — configure repository secrets for CI/CD
