Skip to content

GitHub Codespaces

The devcontainer is pre-configured for GitHub Codespaces — no local setup required.

Accounts you need

Request these before creating your first Codespace. A data team admin grants the GitHub and Google group memberships.

Account What it's for Access
GitHub, TEAMSchools org Repo, Codespaces, pull requests Org member on your role's team (see CONTRIBUTING). The 1Password service account token arrives as a Codespace secret.
Google Workspace (@apps.teamschools.org) BigQuery, local dbt, GCP tooling Member of teamster-analysts@apps.teamschools.org; without it the GCloud setup step fails (see GCloud Authentication).
Claude Claude Code in the Codespace, the Cube connector Claude login; Cube queries also need a cube-* Google group.
1Password Shared team credentials Team account.
dbt Cloud CI run logs on pull requests Read-only.
Dagster+ Runs, assets, branch deployments Read-only.
Asana The Teamster project task Zapier creates for each pull request Workspace member.

Prerequisites

Set VS Code Desktop as your default Codespaces editor:

  1. Go to github.com/settings/codespaces.
  2. Under Editor preference, select Visual Studio Code.
  3. Save.

This ensures Codespaces open in VS Code Desktop (with full extension support) rather than the browser editor.

Creating a Codespace

  1. On the repository page, click Code → Codespaces → Create codespace on main (or your branch).
  2. Select the 4-core / 16 GB machine type.
  3. Wait for the container to finish building. postCreate.sh runs automatically: installs dependencies, bootstraps all dbt projects (dbt deps + dbt parse in parallel), and injects secrets from 1Password. First creation takes a few minutes. VS Code may show a Python interpreter warning during this phase — the base container image seeds a machine-scoped setting pointing to /usr/local/bin/python before the project venv exists. postCreate.sh overwrites that setting early in its run, so the warning is transient and resolves on its own without any action needed.

After the Codespace opens

Do these once, in order. Run each task from Ctrl+Shift+P → Tasks: Run Task.

  1. Sign in to Claude Code in the Claude Code extension panel, and accept the prompt to trust the workspace folder. Trusting the folder lets Claude Code register the plugin marketplaces in .claude/settings.json.
  2. Run GCloud: Application Default Login. It opens a browser for Google OAuth. Required for BigQuery, dbt, and all GCP-authenticated tooling (see GCloud Authentication).
  3. Run Claude: Install Plugins. Claude Code installs most project plugins on its own once the folder is trusted, but not dagster, whose source is a separate git repo. After this, postStart.sh re-runs the install whenever the plugin list in .claude/settings.json changes.
  4. Run dbt: Build Init (see First-time dbt setup).

Dismiss VS Code extension prompts — all required extensions are already configured in devcontainer.json; dismiss any extension install prompts VS Code shows.

Reload the window once background processes finish (Ctrl+Shift+P → Developer: Reload Window) for a clean editor state.

First-time dbt setup

If this is your first Codespace, you need to build your personal dev datasets in BigQuery. Run the dbt: Build Init task:

  1. Ctrl+Shift+P → Tasks: Run Task → dbt: Build Init
  2. Select a project (or all for the full build).

Regional projects (Camden, Miami, Newark, Paterson) build in parallel; kipptaf builds last since it depends on them.

Manual tasks

Run these from Ctrl+Shift+P → Tasks: Run Task:

Task When to use
GCloud: Application Default Login Re-authenticate after token expiry
Claude: Install Plugins Manually re-install project plugins
dbt: Build Init Rebuild dev datasets (pick projects)

Claude Code sign-in is handled by the extension.

Subsequent sessions

postStart.sh runs automatically on resume: it updates uv, syncs dependencies, and re-installs Claude Code plugins if the plugin list changed. It does not refresh credentials; run GCloud: Application Default Login if a GCP call fails with an auth error.

Container Internals

Setup Lifecycle

  • postCreate.sh runs once on container creation — installs dependencies, removes sudo
  • postStart.sh runs on every container start — updates tools and dependencies, re-installs Claude Code plugins when the list changes. Keep it idempotent.

Secret Injection

Secrets are fetched on demand from 1Password by tests/conftest.py when pytest runs. The 1Password service account token is saved to /etc/secret-volume/.op-token during postCreate and revoked from interactive shells in postStart.

  • Adding a new secret: update _FILE_SECRETS in tests/conftest.py and the .env.tpl template in .devcontainer/tpl/.

1Password CLI Commands

  • op inject — replaces op:// references in template files; used for .env.tpl and other templates with embedded secret URIs
  • op read — reads a single secret or file attachment by op:// URI; supports --out-file for binary output; use for multi-attachment items (e.g., op read "op://vault/item/filename" --out-file path)
  • op document get — downloads an item's document attachment by item name/UUID (not op:// URIs); no --file-name flag exists, so it cannot select among multiple attachments — use op read instead

GCloud Authentication

ADC in Codespaces works via user-impersonation of the service account codespaces@teamster-332318.iam.gserviceaccount.com. Access is split into two independent layers:

  • Developer access: all members of teamster-analysts@apps.teamschools.org are granted roles/iam.serviceAccountTokenCreator on the SA, which allows them to impersonate it via --impersonate-service-account
  • SA permissions: codespaces@ has its own direct IAM bindings on the project — it is not a member of teamster-analysts@ and does not inherit group roles. This prevents a self-impersonation loop and keeps the SA's permissions explicitly auditable in IAM.

To grant a new developer access: add them to teamster-analysts@apps.teamschools.org.

Two independent credential stores

gcloud auth login and gcloud auth application-default login write to different places and expire independently:

Store Written by Read by
credentials.db gcloud auth login gcloud CLI commands, bq
application_default_credentials.json gcloud auth application-default login client libraries, local dbt, the gke and gcp-observability MCP servers

A Workspace Cloud-session-length policy expires the user store roughly daily. ADC is unaffected. The symptom is lopsided: bq and bare gcloud start failing with "Reauthentication failed" while local dbt, the ADC-backed MCP servers, and client-library scripts keep working normally.

The GCloud: Application Default Login task refreshes ADC only — there is no task for the user store, so re-run gcloud auth login yourself when you need bq.

Prefer ADC when writing tooling. A script that shells out to gcloud inherits the daily expiry; one that calls google.auth.default() does not. .claude/skills/dagster-day2/scripts/day2_collect.py is the worked example — it authenticates every GCP call with ADC for exactly this reason. Note that ADC resolves to the codespaces@ service account, whose IAM is narrower than your own, so a call that worked under your user credential can still 403 under ADC.

GCloud quirks
  • To check if ADC is valid, use gcloud auth application-default print-access-token; gcloud auth list only checks user accounts, not ADC — they're independent
  • bq CLI requires gcloud auth login (user credentials), not just ADC
  • Running gcloud auth login before gcloud auth application-default login in the same script causes "This app is blocked" errors — keep them separate
  • bq ls paginates at ~50 rows by default — use --max_results=N for large result sets
  • gcloud auth application-default login doesn't work in Codespaces — the localhost OAuth callback can't reach the container. gcloud-application-default-login.sh uses --impersonate-service-account=codespaces@teamster-332318.iam.gserviceaccount.com to work around this; the user must have roles/iam.serviceAccountTokenCreator on that SA
  • The default gcloud OAuth client blocks drive.* scopes — never add them to gcloud auth application-default login without --impersonate-service-account
  • gcloud auth application-default set-quota-project fails with impersonated credentials — use --billing-project flag on the login command instead

Claude Code Setup

  • CLAUDE_CODE_OAUTH_TOKEN (not ANTHROPIC_AUTH_TOKEN) is the correct env var for OAuth token auth — use as a personal Codespace secret to bypass credential store entirely
  • GitHub Actions workflows use claude_code_oauth_token input (not anthropic_api_key)
  • The CLI detects op on $PATH and tries to use it as a credential backend — OP_SERVICE_ACCOUNT_TOKEN is set to a dummy value after secret injection (postStart.sh) to make op fail fast instead of prompting
  • claude-plugins-official must be listed in .claude/settings.json extraKnownMarketplaces (pointing to anthropics/claude-plugins-official) so claude-install-plugins.sh registers it via marketplace add before installs run — omitting it causes "Plugin not found in marketplace" errors for all *@claude-plugins-official plugins
  • claude-install-plugins.sh must never pass --scope project. That scope rewrites the entries in .claude/settings.json and drops fields such as autoUpdate. postStart.sh re-runs the script whenever those entries change, so the rewrite repeats on every container start. The script uses the user scope for marketplaces and --scope local (gitignored .claude/settings.local.json) for plugins
  • Check Claude auth with "${CLAUDE}" auth status | grep -q '"loggedIn": true'; flag-file guards are redundant and produce false "not logged in" errors — check auth directly

Container Quirks

  • apt-get and DAC_OVERRIDE: devcontainer features run apt-get update during docker build, leaving stale files in /var/lib/apt/lists/partial/ owned by _apt:root with mode 0700. --cap-drop=DAC_OVERRIDE would prevent root from accessing these directories at runtime — do NOT add it back. DAC_OVERRIDE is safe to keep because sudo is removed at the end of postCreate.sh, making the capability irrelevant after setup.
  • sudo removed: at the end of postCreate.sh — privileged setup (gcloud components, Helm) must go in postCreate.sh, not later. To add new components, update postCreate.sh and rebuild the container.
  • /etc/secret-volume tmpfs permissions: mounted 0777 (world-writable) so postCreate.sh can write the token file without sudo. File-based secrets are written 600 (owner-read-only) by tests/conftest.py on demand. uid/gid mount options were not used — they are not supported on all Codespaces hosts.
  • --cap-add stripped: Codespaces silently strips --cap-add from runArgs — namespace-based sandboxing (bwrap, unshare) will not work.
  • dbt Core Tools extension: activates on workspaceContains:**/dbt_project.yml and parses projects on startup. Risk: extension may activate before uv sync installs dbt-core.
  • uv cache lives outside the repo: UV_CACHE_DIR points at /workspaces/.uv-cache so the cache sits on the same filesystem as the venvs, letting UV_LINK_MODE=hardlink share package files instead of copying them into every worktree .venv. Both settings come from devcontainer.json, so a change to either needs a container rebuild. The rebuild discards the old cache at ~/.cache/uv along with the rest of the home directory, so there is nothing to clean up afterwards. If a host ever puts the cache and /workspaces on different filesystems, uv warns and falls back to copying rather than failing.
  • A rebuild wipes the home directory: /home/vscode sits on the container overlay and only /workspaces is on the persistent volume, so a rebuild deletes ~/.claude — every Claude Code session transcript, the stored credentials, and ~/.claude.json. Archive them onto the volume first, then restore after the rebuild:
tar -czf /workspaces/claude-backup.tar.gz -C /home/vscode .claude .claude.json
# rebuild the container, then:
tar -xzf /workspaces/claude-backup.tar.gz -C /home/vscode