Back to Rules
πŸ“‹

Teamster CLAUDE.md

A modern data stack for K-12 education

TEAMSchools

by @TEAMSchools

Sourced from TEAMSchools/teamster β€” Apache-2.0

View profile
CLAUDE.md
> Sourced from [TEAMSchools/teamster](https://github.com/TEAMSchools/teamster) β€” [Apache-2.0](https://github.com/TEAMSchools/teamster/blob/3182eb18dd92165f6db0ee69a8f105d0c8980e9b/src/teamster/libraries/alchemer/CLAUDE.md).

# CLAUDE.md

## Layout

```text
src/
  teamster/   # Dagster orchestration code (Python)
  dbt/        # dbt projects, one per warehouse target
tests/        # pytest suites
docs/         # MkDocs site (the "docs" folder; NOT CLAUDE.mds)
.claude/      # Hooks, settings, skills
```

**Read the relevant subdirectory CLAUDE.md before any work there** (reading,
explaining, reviewing, or modifying). Project-wide conventions live in this
file; domain specifics live in the nearest subdirectory CLAUDE.md.

### Subdirectory CLAUDE.mds

| Path                                                                              | Covers                                                                              |
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
| `src/teamster/CLAUDE.md`                                                          | Dagster code: library/code-location pattern, Python standards, asset key convention |
| `src/teamster/code_locations/<name>/CLAUDE.md`                                    | Per-district specifics (read before touching that location)                         |
| `src/dbt/CLAUDE.md` + `src/dbt/<project>/CLAUDE.md`                               | dbt project conventions per warehouse                                               |
| `tests/CLAUDE.md`                                                                 | Test layout and fixtures                                                            |
| `.claude/CLAUDE.md`                                                               | Hook protocol, protected paths, scratch dir                                         |
| `.devcontainer/`, `.github/`, `.k8s/`, `.trunk/`, `scripts/`, `docs/` `CLAUDE.md` | Domain-specific operational context                                                 |

## Working Conventions

- **PII stays local.** Never emit PII values (or screenshots/logs containing
  them) to PR comments, commits, issues, Slack, Asana, scheduled-agent outputs,
  or any other external surface. Local artifacts (`.claude/scratch/`,
  `.worktrees/`, terminal) are fine. Substitute redacted labels (`Student A`,
  `a sample student`) or column-name references. Aggregates / deidentified β‰ 
  PII.
  - **Redaction pass before external writes**: when an external write touches
    values from local validation work, replace PII values with labels or
    column-name references before sending.
  - **What counts as PII** β€” `config.meta.contains_pii: true` in model YAML is
    authoritative but **incomplete**. Untagged columns are PII under FERPA's
    direct-identifier list
    ([34 CFR Β§99.3](https://www.ecfr.gov/current/title-34/part-99/section-99.3)):
    name, SSN, student/employee ID, address, date/place of birth, mother's
    maiden name, biometric record, plus "other information... linked or linkable
    to a specific student." Schema mapping: IDs (`student_number`,
    `employee_number`, `ssn`, `state_id`, `local_id`, source aliases like
    kippadb `school_specific_id`), names (`*_name`), contact (`email`, `phone`,
    `address`, `street`, `city`, `zip`), `dob`/`birth_date`, guardian/parent
    fields, free-text `comment`/`note` on people tables, credentials/tokens.
    When unsure, consult [PTAC glossary](https://studentprivacy.ed.gov/glossary)
    or treat as PII.
  - **Indirect identifiers** β€” FERPA's "linked or linkable" standard
    ([34 CFR Β§99.3](https://www.ecfr.gov/current/title-34/part-99/section-99.3),
    [PTAC glossary](https://studentprivacy.ed.gov/glossary)) covers combinations
    of gender, birth date, geographic indicators (school, zip), race/ethnicity,
    religion, place of birth, education info (grade level, EL status,
    IEP/504/disability), financial info (FRL status), activities, and other
    descriptors that allow identification with "reasonable certainty" by someone
    in the school community. Each field alone may be safe; the combination may
    not. When unsure, consult the linked guidance or treat the combination as
    PII.

- **Before writing any spec or plan**: STOP and explicitly ask the user whether
  to open a GitHub issue first. Required for specs/plans; not required for quick
  fixes. Do not write anything until the user answers. If opening: use
  `mcp__github__issue_write`; label with conventional commit type, related
  source systems, and `dagster`/`dbt` when applicable.

- **Before creating a branch**: ask the user β€” worktree or branch switch? Do not
  choose for them.

- **Before writing any file (spec, code, config)**: be on the feature branch.

- **Worktree**: `gh issue develop <number> --name <branch>` (no `--checkout`),
  then `git worktree add .worktrees/<branch> <branch>`.

- **Linking an existing remote branch to an issue**:
  `mcp__github__create_branch` and GraphQL `createLinkedBranch` both no-op when
  the branch already exists. Delete the remote branch, then
  `gh issue develop <num> --name <branch>`, then re-push local commits.

- **Worktree commands**: Path-flag-driven tools must name the worktree
  explicitly. Use `git -C <worktree>` on every git call (bare `git` from the
  main repo silently commits to `main`) and
  `uv run dbt ... --project-dir <worktree>/src/dbt/<project>` on every dbt call.
  Otherwise prefer absolute paths.

- **Branch switch**: `gh issue develop <number> --name <branch> --checkout`.

- **Git naming**: Commit messages and branch names use
  [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). Branch
  naming: `<gh-username>/<commit-type>/claude-<brief-description>` (get username
  from `mcp__github__get_me`).

- **Git staging**: Prefer `git add -u` β€” naming protected paths triggers the
  hook, `git add -A` can stage unrelated files. Subagents must name specific
  files in `git add` β€” never `-u`, `-A`, or `.`.

- **Dispatching subagents**: Subagents do not auto-invoke skills. In the
  dispatch prompt, name the exact `Skill` tool calls the subagent must run
  before starting work (e.g. `Skill` with
  skill=`dbt:using-dbt-for-analytics-engineering` for a dbt review). For
  negation goals (remove X, no Y), list anti-patterns explicitly β€” subagents
  otherwise re-introduce familiar idioms (`dbt_utils.deduplicate`,
  `select distinct`, `qualify row_number()=1`).

- **Git resuming**: Before resuming work on an existing branch, merge `main`:
  `git fetch origin main && git merge origin/main`.

- **Pull requests**: Squash merge. Use `.github/pull_request_template.md` as the
  PR body.

- **Python**: Always `uv run` β€” never bare `python`, `python3`, or
  venv-installed tools (`dbt`, `dagster`, etc.).

- **Transient Python deps**: Use `uv run --with <pkg> python script.py` for
  one-off scripts needing a package not in `pyproject.toml` β€” don't
  `uv add --dev` for throwaway tooling.

- **Built-in tools over Bash**: Use dedicated tools for file I/O (Read, Grep,
  Glob, Edit, Write). Bash is only for commands with no dedicated tool (`git`,
  `uv run`, `gh`, `docker`, `trunk`, `ls`).

- **Verify tool-call results for resource creation/update**: syntax errors in
  structured tool-call parameters (malformed closing tags, misnested blocks) can
  silently produce corrupted values β€” the call succeeds without error, just with
  the wrong payload. After any call that creates or updates a resource with
  string fields (issue title, PR body, commit message, etc.), check the returned
  values match intent before moving on.

- **Trunk linting/formatting**: Do not run `trunk fmt` or `trunk check` manually
  β€” `trunk-fmt-pre-commit` formats at commit time and `trunk-check-pre-push`
  blocks bad pushes, both in the main repo and in worktrees (`core.hooksPath` is
  shared). **Pre-commit hook runs `fmt` only**; sqlfluff/yamllint and other
  check-only linters fire at `pre-push` and in CI. If a session reports "trunk
  clean" on a SQL/YAML change based on commit hooks alone, run
  `.trunk/tools/trunk check --force <files>` to verify before claiming the
  change is lint-clean.

- **Linter**: Use `# trunk-ignore(<linter>/<rule>)` with a reason comment β€” not
  linter-native disable syntax. Binary:
  `/workspaces/teamster/.trunk/tools/trunk`.

- **Markdown**: Always specify a language on fenced code blocks (MD040). Use
  `text` only when no real language applies.

- **Claude CLI**: Not on `$PATH` β€” user must run `claude` commands in their
  terminal, not via Bash tool.

- **Verify before claiming**: Read actual source code β€” do not extrapolate
  third-party tool behavior from general knowledge.

- **Docs**: "docs" means the `docs/` folder (MkDocs site), not CLAUDE.md files.

## Superpowers skill overrides

- **Spec/plan write order**: When `superpowers:brainstorming` reaches "Write
  design doc" or `superpowers:writing-plans` reaches "Save plan," pause first
  and run the issue-and-branch flow: (a) ask whether to open a tracking issue,
  (b) ask worktree-or-switch, (c) create the branch via
  `gh issue develop <num> --name <branch>`, (d) enter the worktree or check out
  the branch, (e) then write to `docs/superpowers/specs/...` or
  `docs/superpowers/plans/...` on that branch.

- **Worktree consent**: `superpowers:using-git-worktrees` asks "Would you like
  me to set up an isolated worktree?" β€” that's the project's "worktree or branch
  switch?" question. Either answer, the branch must be created via
  `gh issue develop <num> --name <branch>` so it's linked to the issue. Never
  `git worktree add -b` or `git checkout -b` standalone.

- **`finishing-a-development-branch` verification gate**: Skip the skill's
  `npm test / pytest / ...` heuristic. For dbt changes,
  `uv run dbt build --select <model>+` against the relevant project. For Python
  changes, `uv run pytest` where tests exist. PR body uses
  `.github/pull_request_template.md`.

- **Continuous execution exceptions**: `superpowers:subagent-driven-development`
  and `superpowers:executing-plans` say "do not pause between tasks." Pause
  anyway to ask the user before (a) opening a tracking issue, (b) creating a
  branch or worktree, (c) editing any CLAUDE.md file, (d) modifying protected
  files (hook scripts, `.devcontainer/scripts/`, `.claude/settings*.json`).

## CLAUDE.md Editing Rules

- **Before editing any CLAUDE.md file**: present the proposed change as a quote
  block. Do not apply it until the user approves.

- **CLAUDE.md is for Claude, not humans**: cut motivation, rationale, and
  history written to explain the project to a human reader. Keep them only when
  they measurably change Claude's behavior.

- **Before adding to any CLAUDE.md file**: answer the question: "what specific
  decision or action will Claude make differently because of this line?" If you
  can't name one, cut it.

## MCP Servers

Dagster+ MCP auth: do not revert `.mcp.json` to `op run` β€”
`OP_SERVICE_ACCOUNT_TOKEN` is scrubbed post-boot, so `op run` silently breaks
after the first Codespace restart. Keep `scripts/dagster-mcp-launch.sh` as the
launcher. Package internals: see
[TEAMSchools/dagster-plus-mcp](https://github.com/TEAMSchools/dagster-plus-mcp).

- **MCP outages**: If an MCP tool returns "server disconnected" or clearly
  impaired responses, surface to the user before working around with raw `gh` /
  BigQuery calls.

### MCP tool selection

Use BigQuery MCP for ad-hoc queries against known production tables. Use dbt
MCP's `show` only when `ref()` / `source()` resolution is needed β€” it adds
compilation overhead.

For run-internal timelines (steps, engine events, failures), use
`mcp__dagster__get_run_logs` β€” its events are canonical and structured. Note the
unit mismatch: GraphQL `creationTime/startTime/endTime` are float seconds;
`get_run_logs` event `timestamp` is a millisecond string.

GitHub MCP (`mcp__github__*`) is mandatory for any GitHub operation that has an
MCP equivalent. Before running `gh <subcommand>` via Bash, check the
`mcp__github__*` tool list β€” if a matching tool exists, use it.

`gh` via Bash is permitted only when no MCP equivalent exists. Current cases:

- `gh issue develop` β€” linked branch creation; `mcp__github__create_branch` does
  not link branches to issues.
- `gh project item-edit --id <ITEM_ID> --project-id <PROJECT_ID> --field-id <FIELD_ID> --single-select-option-id <OPTION_ID>`
  β€” ProjectV2 field mutations (Status / Tier / Driver / etc.) aren't exposed by
  `mcp__github__*`. To unset a field value (any type), replace the value flag
  with `--clear`. No output on success β€” verify via `gh api graphql` querying
  the item's `fieldValues`. `gh project item-list` JSON also omits ProjectV2
  custom fields whose names contain spaces (e.g. `PR batch`); single-word custom
  fields (`Driver`, `Tier`, `Status`) do appear. Use the same `fieldValues`
  GraphQL query to read the omitted ones.
- `gh project item-add <PROJECT_NUMBER> --owner <OWNER> --url <ISSUE_URL>` β€”
  adds an issue/PR to a ProjectV2 board. No `mcp__github__*` equivalent. Combine
  with `gh project item-edit` to set fields after add.
- `gh run *` β€” Actions run inspection/control; no MCP coverage.
- `gh workflow *` β€” Actions workflow inspection/dispatch; no MCP coverage.
- `gh repo edit` β€” repo settings; `gh repo create/view/list` have MCP
  equivalents and are not on this list.
- Editing an existing comment β€” `mcp__github__add_issue_comment` only creates.
  Use `gh api -X PATCH repos/<owner>/<repo>/issues/comments/<id> -f body='...'`.
- Replying to a PR inline review comment in-thread β€”
  `mcp__github__add_issue_comment` posts top-level PR comments only, not thread
  replies. Use
  `gh api -X POST repos/<owner>/<repo>/pulls/<pr>/comments/<id>/replies -f body='...'`.

### Dagster asset diagnosis

When verifying failures, fetch the most recent run per job (`list_runs` with
`job_name=..., limit=1`, no status filter) β€” bulk cross-referencing capped
result sets misses retries and recoveries.

Asset keys do NOT include dbt subdirectory layers (`staging/`, `intermediate/`,
or mart `facts`/`dimensions`/`bridges`) β€”
`kipptaf/people/int_people__location_crosswalk` (not `.../intermediate/...`) and
`kipptaf/marts/fct_x` (not `kipptaf/facts/fct_x`).

### Dagster Cloud GraphQL (direct, not via MCP)

Host is `kipptaf.dagster.cloud/<deployment>/graphql` (org is `kipptaf`).
`assetChecksOrError` is nested under `assetNodeOrError`; the evaluation success
field is `success` (not `successful`).

### GKE MCP

Authenticates as impersonated service account
`codespaces@teamster-332318.iam.gserviceaccount.com`. If `PermissionDenied`,
check the `CodespacesRole` custom IAM role, not user IAM bindings.

`mcp__gke__query_logs` uses snake_case keys in `time_range` (`start_time`,
`end_time`), not camelCase. Results cap at 100 β€” paginate by using the last
entry's timestamp as the next `start_time`.

`query_logs` format templates reject hyphens in dotted key paths
(`{{.labels.k8s-pod/dagster/op}}` fails to parse). Use the Go template `index`
function instead: `{{index .labels "k8s-pod/dagster/op"}}`. Fall back to full
JSON + jq only when nesting is deeper than `index` can express.

For pod-level logs, prefer `mcp__gke__query_logs` over
`mcp__observability__list_log_entries` β€” the GKE MCP returns pod labels (run-id,
op, code-location) that the observability MCP does not.

### Observability MCP

If any tool returns permission denied, flag it to the user β€” don't assume no
data. `list_time_series` `alignmentPeriod` must end with `s` (e.g., `"60s"` not
`"60"`). Container metrics (`kubernetes.io/container/*`) are keyed by `pod_name`
β€” no `node_name` label; use `kubernetes.io/node/*` for node-level data.

### BigQuery MCP

Truncates results at 50 rows. When querying `INFORMATION_SCHEMA.COLUMNS` for
wide tables, paginate with `WHERE ordinal_position > N`.

Pre-merge queries against PR-branch schema use
`dbt_cloud_pr_<ci_id>_<pr_num>_<schema>` β€” prod `<schema>` lacks unmerged
renames.

Chained joins through PR-branch marts (mart-view β†’ mart-view β†’ upstream-view)
hit BigQuery's 16-view nesting limit. Query materialized prod tables instead, or
split the query.

For NULL-safe distinct counts on composite keys, use
`count(distinct format("%T|%T", a, b))` β€” `concat()` returns NULL when any arg
is NULL and silently miscounts violations.

### dbt MCP

Auth via `scripts/dbt-mcp-launch.sh` β€” do not add `DBT_TOKEN` to `.mcp.json`
directly. `list_jobs` is hard-filtered to `DBT_PROD_ENV_ID`, currently staging
(70403104014899); per-call `environment_id` / `project_id` args exposed by the
schema are ignored. Run-inspection tools (`list_jobs_runs`,
`get_job_run_details`, `get_job_run_error`) ignore env scope and work across
environments by `job_id` / `run_id`. For successful runs, call
`get_job_run_error` with `warning_only=true` to surface test warnings β€”
status=Success does not mean warning-free.

For job inspection, query Staging env (70403104014899) by job id β€” Production
env (70403104000025) has no scheduled dbt Cloud jobs.

Job config changes must go through the dbt Cloud UI β€” no mutation tools exist in
the MCP. Live step logs (`debug_logs`, `structured_logs`) and
`list_job_run_artifacts` return nothing until `artifacts_saved: true` β€” don't
try to diagnose in-flight runs.

Add to your project

Paste into your project's CLAUDE.md or ~/.claude/CLAUDE.md for global rules.

More for Python

MCP servers for Python

Browse all MCP servers β†’

Browse by Tag

Get the Claude Code Starter Pack

Top CLAUDE.md rules for Next.js, TypeScript, Python, Go, and React β€” free.