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:
- Go to github.com/settings/codespaces.
- Under Editor preference, select Visual Studio Code.
- Save.
This ensures Codespaces open in VS Code Desktop (with full extension support) rather than the browser editor.
Creating a Codespace
- On the repository page, click Code → Codespaces → Create codespace on main (or your branch).
- Select the 4-core / 16 GB machine type.
- Wait for the container to finish building.
postCreate.shruns automatically: installs dependencies, bootstraps all dbt projects (dbt deps+dbt parsein 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/pythonbefore the project venv exists.postCreate.shoverwrites 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.
- 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. - Run GCloud: Application Default Login. It opens a browser for Google OAuth. Required for BigQuery, dbt, and all GCP-authenticated tooling (see GCloud Authentication).
- 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.shre-runs the install whenever the plugin list in.claude/settings.jsonchanges. - 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:
- Ctrl+Shift+P → Tasks: Run Task → dbt: Build Init
- 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.shruns once on container creation — installs dependencies, removessudopostStart.shruns 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_SECRETSintests/conftest.pyand the.env.tpltemplate in.devcontainer/tpl/.
1Password CLI Commands
op inject— replacesop://references in template files; used for.env.tpland other templates with embedded secret URIsop read— reads a single secret or file attachment byop://URI; supports--out-filefor 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 (notop://URIs); no--file-nameflag exists, so it cannot select among multiple attachments — useop readinstead
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.orgare grantedroles/iam.serviceAccountTokenCreatoron 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 ofteamster-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 listonly checks user accounts, not ADC — they're independent bqCLI requiresgcloud auth login(user credentials), not just ADC- Running
gcloud auth loginbeforegcloud auth application-default loginin the same script causes "This app is blocked" errors — keep them separate bq lspaginates at ~50 rows by default — use--max_results=Nfor large result setsgcloud auth application-default logindoesn't work in Codespaces — the localhost OAuth callback can't reach the container.gcloud-application-default-login.shuses--impersonate-service-account=codespaces@teamster-332318.iam.gserviceaccount.comto work around this; the user must haveroles/iam.serviceAccountTokenCreatoron that SA- The default gcloud OAuth client blocks
drive.*scopes — never add them togcloud auth application-default loginwithout--impersonate-service-account gcloud auth application-default set-quota-projectfails with impersonated credentials — use--billing-projectflag on the login command instead
Claude Code Setup
CLAUDE_CODE_OAUTH_TOKEN(notANTHROPIC_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_tokeninput (notanthropic_api_key) - The CLI detects
opon$PATHand tries to use it as a credential backend —OP_SERVICE_ACCOUNT_TOKENis set to a dummy value after secret injection (postStart.sh) to makeopfail fast instead of prompting claude-plugins-officialmust be listed in.claude/settings.jsonextraKnownMarketplaces(pointing toanthropics/claude-plugins-official) soclaude-install-plugins.shregisters it viamarketplace addbefore installs run — omitting it causes "Plugin not found in marketplace" errors for all*@claude-plugins-officialpluginsclaude-install-plugins.shmust never pass--scope project. That scope rewrites the entries in.claude/settings.jsonand drops fields such asautoUpdate.postStart.shre-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-getandDAC_OVERRIDE: devcontainer features runapt-get updateduringdocker build, leaving stale files in/var/lib/apt/lists/partial/owned by_apt:rootwith mode0700.--cap-drop=DAC_OVERRIDEwould prevent root from accessing these directories at runtime — do NOT add it back.DAC_OVERRIDEis safe to keep becausesudois removed at the end ofpostCreate.sh, making the capability irrelevant after setup.sudoremoved: at the end ofpostCreate.sh— privileged setup (gcloud components, Helm) must go inpostCreate.sh, not later. To add new components, updatepostCreate.shand rebuild the container./etc/secret-volumetmpfs permissions: mounted0777(world-writable) sopostCreate.shcan write the token file without sudo. File-based secrets are written600(owner-read-only) bytests/conftest.pyon demand.uid/gidmount options were not used — they are not supported on all Codespaces hosts.--cap-addstripped: Codespaces silently strips--cap-addfromrunArgs— namespace-based sandboxing (bwrap, unshare) will not work.- dbt Core Tools extension: activates on
workspaceContains:**/dbt_project.ymland parses projects on startup. Risk: extension may activate beforeuv syncinstalls dbt-core. - uv cache lives outside the repo:
UV_CACHE_DIRpoints at/workspaces/.uv-cacheso the cache sits on the same filesystem as the venvs, lettingUV_LINK_MODE=hardlinkshare package files instead of copying them into every worktree.venv. Both settings come fromdevcontainer.json, so a change to either needs a container rebuild. The rebuild discards the old cache at~/.cache/uvalong with the rest of the home directory, so there is nothing to clean up afterwards. If a host ever puts the cache and/workspaceson different filesystems, uv warns and falls back to copying rather than failing. - A rebuild wipes the home directory:
/home/vscodesits on the container overlay and only/workspacesis 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