- Builds on 6 months of production experience running AI-assisted dev at scale
- We've learned what works, what breaks, what can't be skipped
- Payoff: a checklist to apply these patterns to your own projects
- Thesis: these risks aren't inherent to AI - right controls = AI *and* security
- Hallucination example: Copilot suggested a .NET API that didn't exist, broke the build
- Secret leakage: detect-secrets alone isn't enough - need gitleaks too (complementary regex)
- Supply chain: lookalike packages / poisoned dependency resolution → malicious installs, postinstall execution
- Key line: we didn't build these because we distrust AI - we trust the process, and the process must survive both human and machine error
- Layered defense: if one layer fails, others still protect you
- Not belt-and-suspenders paranoia - it's insurance
- Example: dev forgets pre-commit → CI's secret scan still catches it
- Example: devcontainer escapes capability restrictions → AI config + pre-commit are independent backstops
- Track record: 6 months production use, zero confirmed security incidents
- Source: code.visualstudio.com/docs/devcontainers/containers
- Ground the audience here before Layer 1 - rest of hardening only makes sense once this split is clear
- VS Code UI/theme extensions never leave local OS - exposed port connects to "VS Code Server" inside container
- Workspace extensions, terminal, app, debugger, file system all run inside container boundary (source via volume mount, not copy)
- Copilot (CLI, Chat, or editor agent) inherits the container's capabilities - NOT the host's
- Why this matters: harden this box (cap-drop, no-new-privileges, avoid DinD) and you've bounded everything Copilot can touch
- Mirrors the reference config: minimal Ubuntu base, run with init, drop all capabilities, enforce no-new-privileges
- Secrets injected at runtime via env-file (Key Vault in production) - never committed to devcontainer.json
- Point: least privilege + secret hygiene by default, not optional team discipline
- Ordering is intentional: avoid local container daemons unless you need them
- Container-in-container required? Prefer stronger sandboxing (Sysbox) first
- DinD is the practical fallback (e.g. Aspire workflows)
- DOOD (host socket mount) is out-of-policy - collapses host/container boundary, weakens every other control in this layer
- Reference config separates host bind mounts (read-only where possible) from named volumes that persist auth state/caches across rebuilds
- Security/usability trade: preserve minimum state needed so devs don't resort to insecure workarounds
- Still keeps secrets out of git, limits writable host surface area
- Devcontainer-level complement to the supply-chain risk from the Problem slide
- Lifecycle scripts (npm postinstall, NuGet MSBuild targets) = code exec the moment install/restore runs - no CVE required
- Bake ignore-scripts=true into devcontainer's .npmrc - applies by default to every install
- Ecosystem catching up: npm v12 (mid-2026), pnpm 10, Yarn 4.14 flip default off - response to "Miasma" worm (postinstall scripts)
- NuGet vector less known: PackageReference closed install.ps1, but build/.targets + .props still auto-import and can carry MSBuild tasks
- Real exploit: "IAmReboot" NuGet/MSBuild abuse campaign
- Mitigation: NuGetAudit + source mapping + PR review of new targets/props files
- Closes a gap reviewers often raise: devcontainer hardening only covers inside the sandbox
- Host machine (esp. Windows + WSL2, most common local setup here) needs equivalent coverage
- WSL2 was a blind spot for MDE until recently - plug-in (via Windows Update) brings WSL file/process/network telemetry into the same timeline, eBPF-based
- Pair with endpoint mgmt: Intune settings catalog (or org's EPP/MDM) to govern allowed WSL distros/versions
- Host firewall rules auto-apply to WSL via mirrored networking (Windows 11 22H2 + WSL 2.0.9+)
- Route devcontainer/AI-agent egress through the same proxy/egress filter - don't let a compromised MCP server or bad package skip network controls just by being inside WSL
- Common failure mode: teams exclude WSL vhdx from real-time AV for performance - reopens the exact blind spot the MDE plug-in closes
- Sets up why this section opens with Copilot CLI runtime permissions before copilot-instructions.md and MCP
- Least privilege is the operating model for Copilot's runtime, not just its instructions file
- From settings-config.md's Copilot CLI tool-pattern syntax: shell(COMMAND) takes first-level subcommand for git/gh
- shell(git:*) matches git push/git pull but NOT gitea
- Bare shell(git) quirk = issue #1736 - the Copilot-specific case for "don't rely on broad allows"
- Use precise subcommand allows, not a wildcard on the binary
- Sandboxing = single highest-value control because it survives a permission-system bypass
- Copilot CLI has NO first-class permissions block (allow/deny/ask arrays) in committed JSON
- Depends instead on launch flags, auto-saved permissions-config.json, and hooks - plan hardening around that gap
- Flags/prompts = ephemeral, per-session
- permissions-config.json / settings.json = persist but still heuristic/advisory
- preToolUse hooks = only layer that's deterministic, enforced before the tool runs
- Why Layer 2 builds toward hooks + sandboxing as the real backstops, not the settings file
- shell(env) deny = PromptArmor-recommended mitigation for the env-bypass incident (covered two slides later)
- Deny-beats-allow and never-alias-yolo both come straight from settings-config.md Stage 1 recommendations
- disableBypassPermissionsMode is real/current (GitHub changelog, June 17 2026) - enterprise admin control, NOT a personal setting
- Lives in org's managed-settings.json (.github-private repo; legacy path .github/copilot/settings.json also works)
- GitHub pulls it automatically for Copilot Business/Enterprise users
- Blocks --yolo/--allow-all, /yolo command, all combined-bypass runtime paths
- Gap to call out if asked: does NOT block individual flags like --allow-all-tools or --allow-all-paths
- Don't imply a developer can set this in their own settings.json
- Hook = deterministic enforcement layer behind the heuristic flags/prompts
- Verified against GitHub Docs (copilot-cli/customize-copilot/use-hooks, copilot-sdk/hooks/pre-tool-use)
- Registers under hooks.preToolUse in .github/hooks/*.json with a bash/powershell script path
- Script receives tool call (toolArgs.command for shell calls), returns {"permissionDecision":"deny",...} to block before it runs
- Live example: deny download-and-execute pipelines (curl | sh) and force-pushes, same as GitHub's own tutorial hook
- Known gap: NOT enforced on sub-agents via the task tool - tracked, documented limitation (settings-config.md cites issue #2392)
- Call this out so team doesn't treat hooks as a complete boundary
- Verified via GitHub Docs (cloud-and-local-sandboxes/configuring-local-sandbox-settings) + June 2 2026 changelog ("sandboxes for Copilot now in public preview")
- MXC = Microsoft Execution Containers - OS-level isolation primitive Microsoft ships across Windows/WSL for agentic workloads generally
- /sandbox opens config UI with General/Filesystem/Network tabs to scope what the sandboxed process can touch
- Copilot-specific, per-session control that sits *inside* whatever container Layer 1 already provides
- Scopes what Copilot itself can do, not just where the dev environment runs
- Status: public preview as of June 2026, subject to change
- copilot-instructions.md turns team conventions into machine-readable constraints
- Two failure modes to focus on:
- Hallucination: require verification before referencing packages/APIs not already in the manifest
- Insecure-by-default codegen: require self-flagging risky patterns (hardcoded secrets, shell injection, string-built SQL, disabled TLS) instead of silently emitting them
- Both examples turn a soft preference into a repeatable, reviewable instruction
- Treat this file like policy-as-code - refine whenever you discover a new failure pattern
- The actual lever that matters: tool scoping, not "good MCP examples"
- Concrete example: remote GitHub MCP server (api.githubcopilot.com/mcp/, see github/github-mcp-server docs/remote-server.md)
- Path segments compose the access grant: / = default toolset, /readonly = force read-only, /x/{toolset} = exactly one toolset, /x/{toolset}/readonly = combine both
- Insiders Mode section: /insiders and /readonly/insiders = same pattern for opt-in early-access tools
- Equivalent X-MCP-Toolsets / X-MCP-Readonly / X-MCP-Insiders headers for clients that can't encode in URL
- Recommend /x/{toolset}/readonly as default for most sessions (research, triage, review prep) - only swap to write-enabled when needed
- Same least-privilege principle as Layer 1 capability drops, applied at the MCP layer - access level declared in the connection itself, not buried in env vars/flags
- From settings-config.md's "Enterprise/org governance" section
- "Registry only" is enforcement, but walkable by editing local config - deterrent/detection control, not a hard boundary
- GitHub's own advice for highest security bar: disable MCP org-wide until that gap closes, rather than trust the registry to hold
- Audit logging isn't automatic - same hooks that block dangerous calls double as the ones you wire up to log every call
- Bake in secret redaction (toolArgs can contain tokens)
- OTel export has to be deliberately turned on - off by default
- If nobody pipes these to centralized logging, "we have audit logs" is theoretical
- Pairs with MCP allowlist slide: registry-only enforcement is bypassable, so audit trail is what catches the bypass after the fact
- Section transition: Layer 1 = perimeter (devcontainer/host), Layer 2 = in-flight behavior (permissions, hooks, sandboxing) - both constrain the agent while it acts
- Layer 3 is different in kind: not stopping Copilot from doing something - it's the safety net inspecting the diff itself, regardless of who/what produced it
- Say out loud: pre-commit doesn't care the code came from AI - a hardcoded secret or unpinned Action is just as bad from a human
- Why it's worth investing in even for teams not using AI yet
- Cheapest place in the pipeline to catch something: instant local feedback beats a red CI run or a review comment 3 days later
- "Boring" checks that prevent common accidents:
- trailing spaces → noisy diffs
- missing newlines at EOF
- YAML/JSON syntax errors caught before CI
- incomplete merge conflict resolution
- accidental large file commits
- obvious PEM headers (RSA keys, etc.)
- line ending inconsistencies that break scripts
- case sensitivity issues on cross-platform teams
- Not glamorous, but every one of these has caused production incidents before
- Why both? detect-secrets = tuned for high-entropy patterns; gitleaks = deeper knowledge of specific token formats (GitHub PATs, Slack webhooks, Azure SAS URLs) - they catch different things
- Real incident 1: detect-secrets missed a GitHub token embedded in a larger string - gitleaks caught it via GitHub-token-specific regex
- Real incident 2: high-entropy hex string not flagged (matched a UUID enumeration in the baseline) - gitleaks caught it via AWS access key format pattern
- Conclusion: you need both
- GitHub Actions are a common attack vector
- pull_request_target runs with write permissions on the base branch - extremely dangerous combined with untrusted code
- Expression injection: user input interpolated directly into a shell command without escaping
- Example: `run: echo ${{ github.event.issue.title }}` where title = `; rm -rf /`
- These tools catch these patterns automatically
- Section marker: closes the three-layer architecture, pivots from "here's what to build" to "here's what it takes to live with it"
- Keep this slide spoken, not read - three words doing the work of the next three slides
- Time = the 7-week build-out wasn't optional overhead, it's the floor
- Scrutiny = none of this self-enforces; a human must keep reading Copilot's output closely enough to catch what it misses
- Memory = scrutiny only compounds if written down - that's what copilot-instructions.md is for
- Most teams will be over-optimistic about timeline - say this explicitly
- The technical work isn't the hard part
- Hard part: discovering what your rules should be, curating baselines (detect-secrets false positives need manual suppression), integrating all the pieces, debugging in CI, refining Copilot constraints through real use
- Plan for it
- Layers 1-3 constrain behavior or catch known-bad patterns - none substitute for a human asking "is this right?"
- Devcontainer can't tell you the agent misunderstood a requirement; a secret scanner can't tell you a SQL query is subtly wrong
- Scrutiny doesn't show up in any config file - it's a team habit
- Most likely to erode under deadline pressure - exactly when you need it most
- Payoff of treating copilot-instructions.md as infrastructure (Layer 2), not documentation
- It's where institutional knowledge persists across people and sessions - Copilot has no memory between sessions by default
- Same logic for detect-secrets/gitleaks baselines: each suppression/new pattern is a small permanent improvement the next person benefits from
- Skip this and you're stuck re-litigating the same incident every few months
- Repo is intentionally minimal - not a tutorial project with lots of code
- It's scaffolding that demonstrates: where secrets scanning goes, how to structure copilot-instructions.md, how pre-commit hooks wire into CI
- Minimal .NET code is deliberate - value isn't in the app code, it's in the *surrounding infrastructure*