setup-devcontainer について
テンプレートから、リポジトリまたはローカルチェックアウト用の共有 Dev Container 設定を生成するスキルです。ワークスペースの検出、既存コンテナの確認、SSH 設定や外向き通信の制限といった選択を案内します。
主な機能
- 01テンプレートから .devcontainer/ を生成
- 02リポジトリとワークスペースの状態検出
- 03既存設定とコンテナの確認
- 04SSH 自動化の任意設定
- 05外向き通信の許可リスト設定
活用シーン
- 01リポジトリ用 Dev Container の作成
- 02GitHub 接続前のローカルチェックアウト設定
- 03再オープン前の残存コンテナ確認
- 04開発コンテナの外向き通信制限
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ken-guru/ skills --skill setup-devcontainerインストール後、Claude Code で/setup-devcontainerを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
CONTEXT.mddocs/adding-a-backing-service.mdadding-firewall-later.mdadding-ssh-later.mdconnecting-local-checkout.mddocs/adr/0001-per-container-ssh-keys.md0002-collapse-tool-container-isolation.md0003-manual-first-deploy-key-registration.md0004-digest-pin-base-image-reject-docker-hardened-images.md0005-network-egress-firewall-and-network-manifest.md0006-capability-drop-reject-readonly-rootfs-and-seccomp.md0007-agent-trust-hub-audit-response.mdscripts/check-skill-sync-targets.shpatch-if-absent.shpatch-json-array-if-absent.shrender-devcontainer.shtest-devcontainer.shtest-firewall-lib.shtest-integration.shtest-network-manifest-domains.shtest-patch-if-absent.shtest-patch-json-array-if-absent.shverify-devcontainer.shscripts/lib/render-lib.shtemplates/allowed-domains.local.txtbash-env.shdevcontainer.jsondevcontainer.with-ssh.jsonDockerfileenv.baseline.exampleREADME(原文)English
プロジェクトの README よりこの README の日本語訳はまだありません。以下は英語の原文です。日本語の紹介はページ上部をご覧ください。
Setup Devcontainer
Generates .devcontainer/ from the templates in templates/:
one Shared Container — a single devcontainer.json and Dockerfile —
that every selected AI CLI installs into via its own independent skill, run
separately after this one. See CONTEXT.md for this skill's
vocabulary (Shared Container, Local Checkout, Shared Checkout, Scaffold,
CLI Skill, Own-Block Contract, Project-Owned Block, Capability Seam, Network
Manifest, Project Mounts).
This skill owns the container definition (devcontainer.json, Dockerfile)
and the shared parts of post-create.sh/post-start.sh/README.md
entirely — no CLI skill ever edits them directly, except for one narrow,
explicit exception (the Capability Seam, runArgs, used by
setup-codex-devcontainer).
1. Resolve the workspace root, then detect the target repo
Before anything else, make sure you're standing in the directory VS Code will actually open for
this devcontainer. Every check and file write from here on — including the stale-container check
in step 2 — trusts $(pwd); getting this wrong lets a stale container or a misplaced
.devcontainer/ slip through silently instead of being caught.
git rev-parse --is-inside-work-tree 2>/dev/null
-
Fails: not a git repo yet (the Local Checkout case below, before
git init). Skip this check entirely and continue to detecting the repo. -
Succeeds: compare the current checkout's root against the repo's main working tree:
git rev-parse --show-toplevel git rev-parse --git-common-dir--git-common-diris the shared.gitdirectory; its parent is the main checkout's root, which is what differs in a linked worktree (each worktree has its own top level but shares this common dir).- Parent of
--git-common-dirmatches--show-toplevel: already at the repo root, or in a plain subdirectory of it.cdto--show-topleveland continue — every$(pwd)reference later in this skill is now correct, no further changes needed. - They differ: this is a linked git worktree — a separate checkout directory sharing this repo's history, common in isolated per-task agent sessions. Don't guess which one VS Code will open (worktree-per-devcontainer is a legitimate setup too). Stop and ask: "This looks like a git worktree, not necessarily the folder you'll open in VS Code for the devcontainer. Which folder will that be? If it's not this one, re-run this skill from there instead — the checks below (stale containers, existing config) only ever look at the current directory."
- Parent of
git remote get-url origin
Parse owner/repo from it (works for both [email protected]:owner/repo.git and
https://github.com/owner/repo forms) — this is {{REPO_SLUG}}. {{REPO_NAME}} is the repo
part alone, used in the volume names.
If there's no origin remote yet, ask one question: "Does a GitHub repository already exist for
this project? If yes, name it (owner/repo) — the container's GH_TOKEN will need access to it.
If no, or you're not ready to connect yet, this becomes a local-only Local Checkout —
connectable to GitHub later (see 'Connecting a Local Checkout to a real GitHub repo')."
- Named an existing repo: resolve
{{REPO_SLUG}}/{{REPO_NAME}}from it, same as the existing-origincase above. - Local Checkout:
{{REPO_SLUG}}stays empty.{{REPO_NAME}}falls back to the local working directory's basename (basename "$(pwd)"), sanitized to Docker's naming rules (lowercase, invalid characters replaced with-) — state the resolved name back to the user as part of your summary; don't decide it silently. Also ask, as a separate yes/no question: "Initialize this workspace withgit init?" Record the answer as{{LOCAL_CHECKOUT_GIT_INIT}}("true"/"false"). If yes, resolve{{GIT_DEFAULT_BRANCH}}from the host'sgit config --global init.defaultBranch— substitute it as an empty string if the host has none configured; always substitute this placeholder with something, even"".
Done when you have {{REPO_SLUG}} (possibly empty) and {{REPO_NAME}} — plus, if Local Checkout,
{{LOCAL_CHECKOUT_GIT_INIT}} and, if that's yes, {{GIT_DEFAULT_BRANCH}}.
2. Discover existing setup
test -f .devcontainer/devcontainer.json && echo "devcontainer already exists"
-
Already exists: this repo already has a Shared Container from a prior run. Nothing to regenerate — CLI installs are handled entirely by the setup-
<tool>-devcontainer skills, and adding SSH to an existing devcontainer is Adding SSH to a tool later. Stop here unless the user is specifically asking to reconfigure the base devcontainer itself.Exception: if
devcontainer.jsonsetsworkspaceFolderwithout a matchingworkspaceMount, it predates the fix below and will fail to mount the repo at all. Patch it in place (add"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=cached"next toworkspaceFolder) and check for a stale container from the earlier failed attempt (see below) — a restart alone won't pick up the fix, since the mount is baked in at container creation.Exception: if
devcontainer.jsonhas nocontainerEnv.BASH_ENVentry, it predates the fix that makesGH_TOKEN(and the rest of.devcontainer/.env) visible to an AI CLI's own tool calls — without it,GH_TOKENonly ever reaches the three lifecycle scripts (postCreate/postStart/ postAttach), never aghcommand an agent runs itself, even right after a token was added and the container rebuilt. Patch it in place: add.devcontainer/bash-env.sh(copy of templates/bash-env.sh, chmod +x), add"containerEnv": {"BASH_ENV": "/workspace/.devcontainer/bash-env.sh"}todevcontainer.json, and add thedevcontainer-env-loadblock from templates/post-create-base.sh to.devcontainer/post-create.shif it isn't already there. Needs Dev Containers: Rebuild Container to take effect.Warning, not an exception (nothing here auto-patches this one): if
devcontainer.jsonhas nobuild.targetfield, it predates the Baseline Containment hardening (--cap-drop=ALLplus a small--cap-addallowlist, unconditionally; the opt-in network firewall; the digest-pinned, multi-stageDockerfile) — the whole container still runs with Docker's full default capability set and an unpinned, single-stage image, regardless of anything the setup-<tool>-devcontainer skills or the "later" docs above do to it. There's no automated backfill for this (a deliberate choice, not an oversight — see the skill's own git history for the reasoning). Tell the user plainly: to actually get the hardening, delete.devcontainer/and regenerate from scratch, rerunning anysetup-<tool>-devcontainerskills and redoing the SSH/firewall answers afterward. -
Doesn't exist: fresh setup, continue to step 3.
Also check for a leftover container from an unrelated prior setup of this same workspace folder —
the Dev Containers CLI labels containers by devcontainer.local_folder=<absolute workspace path>,
independent of what the current config says, so a stale container can survive even after its old
config was deleted or never committed:
docker ps -a --filter "label=devcontainer.local_folder=$(pwd)" --format '{{.ID}} {{.Image}}'
If this returns anything, warn the user before they reopen: a container built from a different
setup won't have the vscode user this setup expects, and reopening fails with a cryptic unable to find user vscode: no matching entries in passwd file. Offer to remove it (docker rm -f <id>),
but don't remove it without asking. Skip this check entirely if docker isn't installed or isn't
running.
The same applies to a container built before the workspaceMount fix above: it fails on reopen
with chdir to cwd ("/workspace") set in config.json failed: no such file or directory, followed
by postCreateCommand ... failed with exit code 127 (the script can't even be reached because the
shell never got into /workspace). The container built successfully, so this isn't caught until
reopen. Same remedy — remove it with docker rm -f <id> after confirming with the user — since the
mount config is fixed at creation and won't update on restart.
3. Ask SSH and firewall layers
Skip the SSH question entirely if {{REPO_SLUG}} (step 1) is empty — Local Checkout, with no
GitHub repo yet to register deploy/signing keys against; tell the user why: "SSH layer isn't
available yet — this repo has no GitHub connection; add it once one exists (see 'Connecting a
Local Checkout to a real GitHub repo')."
Otherwise ask: does this repo need agent-driven git push and signed commits? (Adds
deploy-key/signing-key automation — one shared key pair for the whole container.) Addable later
without redoing anything already generated (see Adding SSH to a tool
later).
Then, independent of the SSH answer and regardless of whether {{REPO_SLUG}} is empty (a Local
Checkout gains just as much from restricting outbound reach as a GitHub-connected repo), ask:
does this repo want to restrict this container's outbound network to an allowlist? (A default-deny
iptables+ipset firewall — every installed CLI Skill's own required hosts, plus npm/GitHub, are
allowed automatically via the Network Manifest; everything else is blocked. Trade-off: an AI CLI or
a tool it shells out to that needs to reach some other host — a package registry mirror, a
project's own API — needs that host added to .devcontainer/allowed-domains.local.txt first, or
the request fails closed instead of silently succeeding.) Recommended default: yes, for any
container running an AI CLI with --dangerously-skip-permissions/YOLO-mode automation, since it
bounds what a misbehaving or compromised process can reach; declining costs nothing today. Addable
later without redoing anything already generated (see Adding the firewall layer
later).
4. Resolve remaining placeholders
{{REPO_SLUG}},{{REPO_NAME}},{{LOCAL_CHECKOUT_GIT_INIT}},{{GIT_DEFAULT_BRANCH}}— from step 1.{{GIT_EMAIL_DEFAULT}},{{GIT_NAME_DEFAULT}}— rungit config --global user.emailandgit config --global user.nameon the host. If either is unset, don't invent a default.
5. Generate the devcontainer
-
.devcontainer/Dockerfile← templates/Dockerfile, copied verbatim (no placeholders) — always this one file, firewall answer or not. It's multi-stage:base(the plain image) andfirewall(extendsbasewith theiptables/ipset/iproute2/dnsutils/aggregatepackages the firewall scripts need, plus asudorule scoped to exactly those two scripts' paths). Which stage actually gets built isdevcontainer.json'sbuild.target, below — not which file was copied — so the Dockerfile itself never needs swapping. -
.devcontainer/bash-env.sh← templates/bash-env.sh, copied verbatim (no placeholders), chmod +x. Loads.devcontainer/.env; referenced asBASH_ENVbelow so every non-interactive shell in the container (an AI CLI's own tool calls, which runbash -c ...and never source~/.bashrc) seesGH_TOKENand friends, not just the three lifecycle scripts. -
.devcontainer/network-manifest.json← templates/network-manifest.json, copied verbatim (no placeholders). This is the Network Manifest (see CONTEXT.md): an object keyed by CLI name, each value holding anetworkAllowlistarray of{host, purpose, source}entries. Seeded here with the universal baseline every container needs regardless of which CLI Skills end up installed —registry.npmjs.org,github.com,api.github.comfor npm/git/gh, plusarchive.ubuntu.com/ports.ubuntu.comso ad hocsudo apt-get install <tool>mid-session (issue #291 story 19) keeps working under the firewall too, not just with it declined; without these two,apt-get updatesilently fails to refresh the package index and only already-cached packages stay installable — filed under a"baseline"key rather than a fake CLI name, since none of this is owned by any one CLI. Each CLI Skill later adds its own keyed entry (a separate concern, not generated here). Generated unconditionally, firewall opt-in or not — a CLI Skill installed later always gets its own entry recorded here, and the firewall (whether added now or later) picks it up automatically with no extra step. -
.devcontainer/network-manifest-domains.sh← templates/network-manifest-domains.sh, copied verbatim (no placeholders), chmod +x. The Network Manifest'sjq-based derivation helper: given a manifest file, prints one deduplicated host per line across every keyed entry'snetworkAllowlist. Also generated unconditionally; sourced byfirewall-lib.sh(below) when the firewall layer is present, otherwise present but unused. -
.devcontainer/project-mounts.local.json← templates/project-mounts.local.json, copied verbatim — an empty JSON array. This is the Project Mounts seam (see CONTEXT.md): a project-owned file, parallel toallowed-domains.local.txtbelow, never touched again by any skill after this. Generated unconditionally (mounts are orthogonal to the SSH/firewall questions). Add this project's owndevcontainer.jsonmountsentries here (raw mount strings, e.g."source=my-project-data,target=/var/lib/data,type=volume", matching the format of themountsentrydevcontainer.jsonalready carries), then fold them intodevcontainer.jsonwith:scripts/patch-json-array-if-absent.sh .devcontainer/devcontainer.json .mounts .devcontainer/project-mounts.local.jsonSafe to re-run after every edit — already-present entries are left untouched. Needs Dev Containers: Rebuild Container to take effect, since
mountsis set at container creation, not read again on a plain restart. -
Add
.devcontainer/project-mounts.local.jsonto.gitignoreif it isn't already ignored (parallel to.devcontainer/.envabove). -
.devcontainer/devcontainer.json← templates/devcontainer.json (or templates/devcontainer.with-ssh.json if step 3's SSH answer was yes), substitute{{REPO_NAME}}. TherunArgsfield starts pre-populated with the unconditional Baseline Containment capability-drop entries (--cap-drop=ALL --cap-add=CHOWN --cap-add=DAC_OVERRIDE --cap-add=FOWNER --cap-add=SETUID --cap-add=SETGID— the last two aresudoitself: its binary is setuid-root, but it still callssetresuid()/setresgid()internally to fully switch identity, which needs those capabilities in the bounding set regardless of the setuid bit, or everysudoinvocation fails outright, including the firewall's own) — not gated behind any setup question — and this is also the Capability Seam, the one extension point a CLI skill (today, onlysetup-codex-devcontainer) may idempotently append its own entries to on top.containerEnv.BASH_ENVpoints atbash-env.shabove — this is what makesGH_TOKENvisible to an AI CLI's own tool calls, not just the lifecycle scripts.initializeCommandinvokes the generated.devcontainer/initialize.sh(below) viash, notbash— it runs on the host, before the container exists, and the script's own content has no bash dependency to preserve.mountsstarts with just the shared config volume; a project's own entries come fromproject-mounts.local.json(below), folded in separately, never hand-added here. If step 3's firewall answer was yes, also patch in the firewall's own capability grant (--cap-add=NET_ADMIN --cap-add=NET_RAW—iptables/ipsetneed them), through the samepatch-json-array-if-absent.shprimitive the Capability Seam itself is built on — not the Capability Seam extension point itself, since this is the base skill writing torunArgsit already owns, not a CLI skill appending on top — applied at generation time rather than via a later CLI-skill patch, since this is conditional on the base skill's own setup question, not on any particular CLI, and switchbuild.targetfrom"base"to"firewall"so the Dockerfile's firewall stage (above) actually gets built:scripts/patch-json-array-if-absent.sh .devcontainer/devcontainer.json .runArgs templates/firewall-capability-entries.json jq '.build.target = "firewall"' .devcontainer/devcontainer.json > .devcontainer/devcontainer.json.tmp \ && mv .devcontainer/devcontainer.json.tmp .devcontainer/devcontainer.json -
.devcontainer/post-create.sh— generated by scripts/render-devcontainer.sh, which assembles the base skeleton (git identity, Local Checkout git-init, the sharedinstall_cli/chown_config_volumefunction definitions, and the SSH block pair if enabled), chmod'ing the result +x:scripts/render-devcontainer.sh \ --repo-name "{{REPO_NAME}}" --repo-slug "{{REPO_SLUG}}" \ --out .devcontainer/post-create.sh \ --initialize-out .devcontainer/initialize.sh \ [--ssh] \ [--git-email-default "<value>"] [--git-name-default "<value>"] \ [--local-checkout-git-init "{{LOCAL_CHECKOUT_GIT_INIT}}"] [--git-default-branch "{{GIT_DEFAULT_BRANCH}}"]Verify with scripts/verify-devcontainer.sh, passing the exact same flags (minus
--out/--initialize-out, plus--file/--initialize-file).This skeleton is what every CLI skill later appends its own install block to. The SSH block never fails the build — an under-scoped or missing
GH_TOKEN(or unsetDEVCONTAINER_HOST) degrades to skipping the rest of the SSH setup, recording why in~/.ssh/.ssh-setup-skipped.The same command also writes
.devcontainer/initialize.sh(chmod +x'd) — the same--initialize-outflag above, unconditional, no--ssh/--firewallvariants. This isinitializeCommand's target (seedevcontainer.jsonabove): today's exact.env-bootstrap behavior, ported into an appendable script file rather than an inline one-liner, so a project needing its own host-side initialization step (something that must run before the container exists, so it can't live inpost-create.sh) can append its own Project-Owned Block the same way it already can forpost-create.sh/post-start.sh:scripts/patch-if-absent.sh append .devcontainer/initialize.sh "# --- Project: <name> ---" <block-file> -
.devcontainer/post-start.sh← templates/post-start-base.sh, copied verbatim, chmod +x. Empty skeleton — each CLI skill appends its own skill-sync block here if the user opts into it for that tool, and (below) the base skill appends the firewall invocation if step 3's firewall answer was yes. -
.devcontainer/post-attach.sh← templates/post-attach.sh, substituted, chmod +x. -
.devcontainer/.env.example← templates/env.baseline.example, substituted. -
.devcontainer/README.md← templates/README.baseline.md, substituted. Its## Installed CLI Toolsheading is deliberately the file's last section, empty — every CLI skill's own bullet is a plain append to end-of-file, landing there without needing to search for a position. -
Add
.devcontainer/.envto.gitignoreif it isn't already ignored.
If step 3's SSH answer was yes:
-
.devcontainer/.env.examplegets templates/env.ssh-block.example appended, idempotently, and itsGH_TOKENcomment gets:Deploy-key registration is manual by default (see the SSH setup prompt after first attach) and needs no extra permission. Optional: grant this repo's Administration (read/write) permission too if you'd rather the deploy key auto-register itself — plus whatever else you use gh for.scripts/patch-if-absent.sh append .devcontainer/.env.example "# Your host machine's hostname — used to label the SSH deploy/signing keys so" templates/env.ssh-block.example -
.devcontainer/README.mdgets templates/README.ssh-block.md, substituted ({{REPO_NAME}},{{REPO_SLUG}}) into a scratch copy first — unlike every other substituted template above, this one is also an input to another command rather than a file written directly, so substitute before patching, not after — inserted before## Installed CLI Tools, not appended to end-of-file — this keeps that section as the file's last one no matter when SSH is added — and the baseline template's closing "SSH deploy key and signing key automation — Not set up here" section is deleted (superseded by the real section):scripts/patch-if-absent.sh insert-before .devcontainer/README.md "## SSH deploy key and signing key" "## Installed CLI Tools" <substituted-copy-of-templates/README.ssh-block.md> scripts/patch-if-absent.sh delete-section .devcontainer/README.md "## SSH deploy key and signing key automation"
If step 3's firewall answer was yes:
-
.devcontainer/init-firewall.sh← templates/init-firewall.sh, copied verbatim (no placeholders), chmod +x..devcontainer/refresh-allowlist.sh← templates/refresh-allowlist.sh, copied verbatim, chmod +x..devcontainer/firewall-lib.sh← templates/firewall-lib.sh, copied verbatim, chmod +x — sourced (not executed directly) by the two scripts above, the single place both build their domain list from (the Network Manifest, vianetwork-manifest-domains.sh, plusallowed-domains.local.txtbelow), so they can't independently drift the way two hand-duplicated lists would. -
.devcontainer/allowed-domains.local.txt← templates/allowed-domains.local.txt, copied verbatim — empty except for its explanatory header comment. This repo's own project-specific network hosts go here; never touched again by any skill after this. -
Add
.devcontainer/allowed-domains.local.txtto.gitignoreif it isn't already ignored (parallel to.devcontainer/.envabove). -
.devcontainer/post-start.shgets templates/post-start-firewall-block.sh appended, idempotently:scripts/patch-if-absent.sh append .devcontainer/post-start.sh "# --- Network egress firewall ---" templates/post-start-firewall-block.sh -
.devcontainer/README.mdgets templates/README.firewall-block.md inserted before## Installed CLI Tools, and the baseline template's closing "Network egress firewall automation — Not set up here" section is deleted (superseded by the real section):scripts/patch-if-absent.sh insert-before .devcontainer/README.md "## Network egress firewall" "## Installed CLI Tools" templates/README.firewall-block.md scripts/patch-if-absent.sh delete-section .devcontainer/README.md "## Network egress firewall automation"
Done when every file above exists, devcontainer.json parses as valid JSON with runArgs
containing exactly the baseline capability-drop entries, plus --cap-add=NET_ADMIN and
--cap-add=NET_RAW if step 3's firewall answer was yes
(jq -e '.runArgs | sort == ["--cap-add=CHOWN","--cap-add=DAC_OVERRIDE","--cap-add=FOWNER","--cap-add=SETGID","--cap-add=SETUID","--cap-drop=ALL"]' .devcontainer/devcontainer.json
declined, or ... sort == ["--cap-add=CHOWN","--cap-add=DAC_OVERRIDE","--cap-add=FOWNER","--cap-add=NET_ADMIN","--cap-add=NET_RAW","--cap-add=SETGID","--cap-add=SETUID","--cap-drop=ALL"]' .devcontainer/devcontainer.json
accepted), build.target is "base" if declined or "firewall" if accepted
(jq -e '.build.target == "base"' .devcontainer/devcontainer.json or ... == "firewall"'), no
{{...}} placeholder remains
in any written file (grep -rn '{{' .devcontainer/), and post-create.sh/initialize.sh pass
scripts/verify-devcontainer.sh for the exact flags they were
rendered with (--file/--initialize-file, plus --project-mounts-file .devcontainer/project-mounts.local.json to confirm it still matches the template verbatim).
6. Report next steps
Tell the user, adapted to whether SSH/the firewall are present:
- Install Docker Desktop and the Dev Containers VS Code extension.
- Copy
.devcontainer/.env.exampleto.devcontainer/.envand fill inGH_TOKEN{{, andDEVCONTAINER_HOST(runhostname) if SSH is present}}. {{On a re-run against an existing container: if.devcontainer/.envstill containsgithub_pat_your_token_hereoryour-hostname-here, replace it with the real value or clear it — an older.env.exampleshipped those placeholders as values, and they're exported as real credentials.}} - Reopen the repo in the container (Dev Containers: Reopen in Container).
- Run whichever
setup-<tool>-devcontainerskill(s) you want, to add AI CLIs. - {{If SSH is present: on attach,
post-attach.shprints setup instructions for whichever of the deploy key and signing key aren't registered yet (the deploy key may already be handled if GH_TOKEN had Administration access) — follow them, dismissing each with thetouchcommand it gives you.}} - {{If the firewall is present: this container's outbound network now default-denies everything
except the Network Manifest's allowlist. Add any of this project's own hosts to
.devcontainer/allowed-domains.local.txtbefore they're needed — a host missing from there, the Network Manifest, or GitHub's/npm's own ranges is unreachable by design.}} - If this project needs its own
devcontainer.jsonmounts (a database data directory, a persistent history volume, etc.), add them to.devcontainer/project-mounts.local.jsonand fold them in withscripts/patch-json-array-if-absent.sh .devcontainer/devcontainer.json .mounts .devcontainer/project-mounts.local.json, then rebuild. If it needs its own host-side initialization step, append it to.devcontainer/initialize.shthe same way you'd append topost-create.sh/post-start.sh. If it needs an in-container backing service (e.g. PostgreSQL), see Adding a backing service.
Adding SSH to a tool later
For a repo that already has a devcontainer and now needs agent-driven git push / signed commits.
See docs/adding-ssh-later.md.
Adding the firewall layer later
For a repo that already has a devcontainer and now wants the opt-in network egress firewall. See docs/adding-firewall-later.md.
Adding a backing service
For a project that needs a backing service (e.g. PostgreSQL) running inside the Shared Container, without reintroducing Docker Compose. See docs/adding-a-backing-service.md.
Connecting a Local Checkout to a real GitHub repo
For a repo generated as Local Checkout (step 1) that now has a real GitHub repository to push to. Needs no skill-level regeneration — see docs/connecting-local-checkout.md.