Claude access¶
Most of the code and configuration in this project is written by Claude (Anthropic's LLM), run via Claude Code inside a dedicated devcontainer. This page describes what that access actually is: scoped and revocable, not blanket admin.
Environment¶
Claude runs Claude Code inside a devcontainer defined in thomelab-infra/docker/devcontainer/
and deployed via thomelab-infra/kubernetes/applications/devcontainer/. It is a separate
instance from Thomas's own devcontainer (devcontainer-claude vs. devcontainer-user), each
reachable at its own subdomain (claude-dev.thomelab.net / thomas-dev.thomelab.net), each
gated by Keycloak SSO restricted to a devcontainer-users group.
Kubernetes¶
Each cluster runs a dedicated claude ServiceAccount in a claude-access namespace, deployed
via ArgoCD like everything else. It is read-only, and scoped by an explicit allowlist:
- Cluster-wide, read-only: namespaces, nodes, PersistentVolumes, StorageClasses, VolumeSnapshots, Velero backup/schedule/restore status, node and pod metrics.
- Namespace-scoped, read-only, only in explicitly listed namespaces: pods and logs, deployments, statefulsets, daemonsets, replicasets, jobs, cronjobs, services, endpoints, configmaps, events, PVCs, ingresses, network policies, Traefik IngressRoutes/middlewares/TCP-UDP routes, ArgoCD Applications, CNPG clusters.
- Secrets are excluded entirely — not read, not listed.
Which namespaces are visible is a per-cluster, explicit list (RoleBinding resources in
thomelab-infra/kubernetes/cluster-tools/claude-access/overlays/<cluster>/), not a default-allow
policy. Adding access to a new namespace is itself a change to thomelab-infra, going through
the same issue/branch/merge-request flow as everything else.
The kubeconfig itself is built from OpenBao-backed ExternalSecrets and mounted read-only into the
devcontainer — there is no locally-stored admin kubeconfig to fall back on. For public, whose
host firewall permits only port 443 by default, the kubeconfig's primary path to the API server
goes through a Tailscale sidecar in the devcontainer talking to Tailscale's Kubernetes-operator
API-server proxy, not a direct connection; a direct-connection context still exists as an
explicit, non-default fallback. The devcontainer's Tailscale identity has its own auth key and
state, separate from Thomas's devcontainer.
Git and CI¶
Access to each repository is a separate GitLab Personal Access Token, one per repo, loaded from
env.sh, rather than one token with access to everything. A token scoped to thomelab-site
cannot touch thomelab-infra.
Every repository's stated convention is the same: no direct commits to main (enforced by branch protection on main). Every change is a
GitLab issue, a branch named after the issue, and a merge request. In practice, this means Claude
opens the issue, does the work on a branch, opens the merge request, and assigns it — merging is
a separate, human action.
Harbor (the container registry) works the same way by omission: Claude has no credentials for
harbor.thomelab.net/thomelab-private, the auth-required project.
Encrypted content¶
git-crypt, ansible-vault, and sops+age are real boundaries here, not just documented ones:
this devcontainer has no GPG private key to unlock git-crypt content (verified — gpg
--list-secret-keys is empty), ~/.vaultpass is mounted only into Thomas's own devcontainer, and
the age private keys (Thomas's personal one, and the dedicated one seeded for chezmoi) are likewise
provisioned only into Thomas's devcontainer, never Claude's. Unlike the OpenBao-backed secrets
Claude is handed (kubeconfig, GitLab tokens), nobody has provisioned it with these.
Destructive operations¶
Force-pushes, git reset --hard, --no-verify, and similar are not run without asking first.
On a branch, this is a session-level operating instruction rather than a platform-enforced
control — unlike the Kubernetes RBAC and per-repo token scoping above, nothing currently prevents
it there at the infrastructure level. main is the exception: branch protection (see above)
blocks a force-push regardless of what Claude does.