Files
ngn-agent/.planning/phases/06-default-repos-ssh-mount/06-RESEARCH.md

32 KiB

Phase 6: Default Repos & SSH Mount — Research

Researched: 2026-06-14 Domain: Docker volume mounts, SSH credential provisioning, shell init scripts Confidence: HIGH

Summary

Phase 6 enables three things: (1) SSH keys mounted into the Hermes Docker container so git operations against Bitbucket work automatically, (2) three default repos (rai-ops, rai-deployment, rai-devtools) mounted directly from the host filesystem into /workspace/ as subpath volumes, and (3) a lightweight session-init.sh script that verifies the mounts are present and logs their status. No new packages are installed — all changes are additive configuration in ~/.hermes/config.yaml plus one new shell script and one .env variable.

Primary recommendation: Append SSH key + repo volumes to terminal.docker_volumes, add session-init.sh to shell_init_files, add DEFAULT_REPOS to docker_forward_env and .env. Verified working end-to-end via Docker tests.

Verified findings from live testing:

  • SSH key mounts (:ro) into /root/.ssh/ with id_ed25519razer, id_rsa, config, and known_hosts authenticate successfully against Bitbucket (authenticated via ssh key). [VERIFIED: Docker test with actual keys]
  • Subpath volume mounts (/workspace/<repo>) work when parent /workspace is :rw. [VERIFIED: Docker test]
  • git clone --depth 1 on-demand into /workspace/ succeeds using mounted SSH keys. [VERIFIED: Docker test]
  • known_hosts must be mounted for host-key verification; bitbucket.org entries are present on this host. [VERIFIED: checked /Users/bapung/.ssh/known_hosts]

<phase_requirements>

Phase Requirements

ID Description Research Support
REPO-01 DEFAULT_REPOS auto-cloned into every new Hermes session Mount approach (not clone): Repos mounted via docker_volumes at /workspace/<name> — preserves worktrees, branches, no re-cloning. session-init.sh via shell_init_files verifies mounts. DEFAULT_REPOS env var forwarded. [VERIFIED: Docker test]
REPO-02 User can request additional repos to clone on demand On-demand flow: Agent runs git clone git@bitbucket.org:razersw/<repo>.git inside Docker — SSH keys are mounted, auth works. Clones persist only for session lifetime (ephemeral on container restart). [VERIFIED: Docker test]
</phase_requirements>

<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • D-01 (Git Auth): Mount specific SSH keys read-only into Docker — ~/.ssh/id_ed25519razer, ~/.ssh/id_rsa, ~/.ssh/config
  • D-02 (Repo Mounts): Mount repo directories directly from host — ~/Razer/rai-ops:/workspace/rai-ops:rw, etc.
  • D-03 (Session Init): Create session-init.sh in ~/.hermes/scripts/, trigger via shell_init_files
  • D-04 (DEFAULT_REPOS): Env var in .envDEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools
  • D-05 (On-Demand): Agent clones additional repos into /workspace/ using git clone — ephemeral (lost on container restart)

the agent's Discretion

  • Init script error handling: Non-blocking — session should still start if repos missing
  • On-demand clone destination: Default to /workspace/ unless specified otherwise

Deferred Ideas (OUT OF SCOPE)

  • Auto-register repos as git worktrees via worktree: true — already handled by host-side git setup
  • Per-repo deploy keys instead of personal SSH key — future security hardening </user_constraints>

Architectural Responsibility Map

Capability Primary Tier Secondary Tier Rationale
SSH key provisioning Host (config.yaml) Keys exist on host FS; Docker volume mounts inject them into the container. No agent-side key handling.
Repo workspace availability Host (config.yaml) Repos mounted via docker_volumes at container start. No runtime cloning for default repos.
Mount verification Container (shell_init) session-init.sh runs inside container at shell start, before agent prompt.
On-demand repo cloning Container (agent) Agent runs git clone inside Docker using mounted SSH keys. Ephemeral.
Credential isolation Host (config.yaml) Container (read-only mount) Keys mounted :ro — agent cannot modify them. Read-only protection enforced by Docker volume mode.

Standard Stack

Core

No new libraries or packages. This phase uses existing infrastructure:

Component Version Purpose Why Standard
Git 2.47.3 Version control operations Already installed in docker image nikolaik/python-nodejs:python3.11-nodejs20 [VERIFIED: Docker test]
OpenSSH 10.0p2 SSH authentication for git Already installed in docker image [VERIFIED: Docker test]
Docker volumes Mount SSH keys and repo directories into container Standard Docker mechanism; Hermes terminal.docker_volumes supports it natively
Hermes shell_init_files Execute script at container shell start Documented Hermes extension point; runs synchronously before shell prompt
Bash test -d Verify mount existence Fastest way to check directory presence; no external dependencies

Supporting

Component Version Purpose When to Use
Hermes docker_forward_env Forward DEFAULT_REPOS env var into container For any env var the agent needs at runtime
~/.ssh/known_hosts SSH host key verification for Bitbucket Must be mounted — without it, SSH prompts for host key confirmation and fails non-interactively [VERIFIED: grep/ssh test]

Alternatives Considered

Instead of Could Use Tradeoff
SSH key mount SSH agent forwarding Needs host socket mount, less reliable (socket path varies), doesn't survive container restart. Key mount is simpler and tested.
Direct repo mount git clone inside container at init Clones are ephemeral (lost on container restart), wastes bandwidth/disk, loses worktrees. Mount preserves host-side git state.
Full ~/.ssh: mount Per-file mount Full directory mount exposes ALL keys (including potential 3rd-party keys). Per-file mount limits exposure to only the keys the agent needs.

Installation: No packages to install. All changes are configuration edits and one script file.

Package Legitimacy Audit

No packages are installed in this phase. All changes are configuration (config.yaml, .env) and one shell script (session-init.sh). No npm, pip, or cargo dependencies required.

Package Registry Verdict Disposition
No packages to verify

Architecture Patterns

System Architecture Diagram

┌─────────────────────────────────────────────────────────────────┐
│  HOST (macOS)                                                    │
│                                                                   │
│  ~/.hermes/config.yaml                                           │
│  ├─ docker_volumes: [list of mounts] ──────────────────────┐     │
│  ├─ shell_init_files: ["/usr/local/bin/session-init.sh"] ─┐│     │
│  └─ docker_forward_env: [..., "DEFAULT_REPOS"] ──────────┐││     │
│                                                            │││     │
│  ~/.hermes/.env                                            │││     │
│  └─ DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools ────┘││     │
│                                                             ││     │
│  ~/.ssh/                                                    ││     │
│  ├─ id_ed25519razer ─────────────────────────────────────┐  ││     │
│  ├─ id_rsa ──────────────────────────────────────────────┐│  ││     │
│  ├─ config ──────────────────────────────────────────────┐││  ││     │
│  └─ known_hosts ────────────────────────────────────────┐│││  ││     │
│                                                         ││││  ││     │
│  ~/Razer/                                               ││││  ││     │
│  ├─ rai-ops/ ───────────────────────────────────────────┘│││  ││     │
│  ├─ rai-deployment/ ─────────────────────────────────────┘││  ││     │
│  └─ rai-devtools/ ───────────────────────────────────────┘│  ││     │
└──────────────────────────────────────────────────────────────┘││     │
                                                               ││     │
                    ┌── Docker Volume Mounts ────────────────────┘│     │
                    │  (host:container:mode)                       │     │
                    │                                              │     │
                    ▼                                              ▼     │
┌──────────────────────────────────────────────────────────────────────┐ │
│  DOCKER CONTAINER (nikolaik/python-nodejs:python3.11-nodejs20)   │  │
│                                                                    │  │
│  /root/.ssh/                                                       │  │
│  ├─ id_ed25519razer (ro) ◄─────────────────────────────────────────┘  │
│  ├─ id_rsa (ro) ◄─────────────────────────────────────────────────────┘
│  ├─ config (ro) ◄──────────────────────────────────────────────────────
│  └─ known_hosts (ro) ◄─────────────────────────────────────────────────
│                                                                    │
│  /workspace/ (rw, from docker_mount_cwd_to_workspace)              │
│  ├─ rai-ops/ (rw) ◄── ~/Razer/rai-ops on host                     │
│  ├─ rai-deployment/ (rw) ◄── ~/Razer/rai-deployment                │
│  ├─ rai-devtools/ (rw) ◄── ~/Razer/rai-devtools                   │
│  │                    [agent workspace — git operations work]      │
│  └─ <clone>/ (rw, ephemeral) ◄── git clone from agent              │
│                                                                    │
│  /usr/local/bin/                                                   │
│  └─ session-init.sh (ro, from ~/.hermes/scripts/)                  │
│       │                                                            │
│       │ Shell start: shell_init_files triggers session-init.sh     │
│       ▼                                                            │
│  Verifies each DEFAULT_REPO mount exists at /workspace/<name>      │
│  Logs status, exits (non-blocking)                                 │
└────────────────────────────────────────────────────────────────────┘
                              │
                              │ On-demand clone (REPO-02):
                              │ git clone git@bitbucket.org:razersw/<repo>.git
                              │ Uses mounted SSH keys → auth OK
                              ▼
                    Bitbucket Cloud (git.razersw atlassian)

This phase adds one file and modifies two existing config files:

~/.hermes/
├── config.yaml              # MODIFY: append docker_volumes, shell_init_files, docker_forward_env
├── .env                     # MODIFY: add DEFAULT_REPOS
└── scripts/
    ├── session-init.sh      # NEW: mount verification script
    ├── ngn-jira             # (existing, unchanged)
    ├── ngn-bitbucket        # (existing, unchanged)
    └── ngn-confluence       # (existing, unchanged)

Pattern 1: Subpath Volume Mounts with Parent Dependencies

What: Mounting host directories at subpaths of an already-mounted parent volume. Docker correctly handles this when the parent volume is :rw.

When to use: When you need to add workspace directories alongside the project's default /workspace/ mount.

Key constraint discovered during testing: The parent mount (/workspace from docker_mount_cwd_to_workspace) must be :rw. If it's :ro, Docker cannot create mount points for subpath volumes:

# FAILS — parent is :ro:
docker run -v /host/project:/workspace:ro -v /host/repo:/workspace/repo:rw
# Error: mkdirat ... read-only file system

# WORKS — parent is :rw:
docker run -v /host/project:/workspace:rw -v /host/repo:/workspace/repo:rw
# ✓ Repo mounted and writable

[VERIFIED: Docker test — parent :ro fails, parent :rw succeeds]

Implication for Hermes: Hermes' docker_mount_cwd_to_workspace: true mounts the project directory to /workspace. The planner must verify this mount is :rw (which it almost certainly is, since the agent writes files to /workspace/).

Pattern 2: Per-File SSH Key Mount (Security-Conscious)

What: Mounting individual SSH key files (rather than the entire ~/.ssh/ directory) to limit credential exposure inside the container.

When to use: Always prefer per-file mounts over directory mounts for sensitive credentials.

# LESS secure — mounts entire .ssh directory
"~/.ssh:/root/.ssh:ro"

# MORE secure — mounts only specific keys
"~/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro"
"~/.ssh/id_rsa:/root/.ssh/id_rsa:ro"
"~/.ssh/config:/root/.ssh/config:ro"
"~/.ssh/known_hosts:/root/.ssh/known_hosts:ro"

[VERIFIED: Docker test — all four files mounted individually, SSH auth works]

Note: The known_hosts file must be mounted too. Without it, SSH prompts interactively for host key confirmation on first connection, which fails inside a non-interactive container. The host's ~/.ssh/known_hosts already contains bitbucket.org's host keys (verified).

Pattern 3: Non-Blocking Shell Init Script

What: A shell_init_files script that runs synchronously before the shell prompt, but uses only fast operations (no network, no git operations) to avoid blocking container startup.

When to use: For any verification task at session start. The script must never block.

#!/bin/bash
# session-init.sh — Non-blocking mount verification
set -uo pipefail    # deliberately NOT set -e — continue on errors

DEFAULT_REPOS="${DEFAULT_REPOS:-}"
# ... lightweight test -d checks ...
exit 0              # always exit cleanly

Anti-Patterns to Avoid

  • Blocking init script: Do NOT git clone or run network operations in session-init.sh. This runs synchronously before the shell prompt — a hanging script prevents the agent from starting. If cloning were needed (not in this phase — we mount instead), wrap with timeout 30.
  • Using set -e in init script: If a repo mount is missing, set -e would cause the script to abort at the first missing directory. Use set -uo pipefail instead and handle failures gracefully.
  • Mounting full ~/.ssh/ directory: Exposes ALL SSH keys (including any from third-party services) to the agent. Per-file mounts limit blast radius.
  • Cloning into mounted repo directories: If the agent clones a repo into a path that shadows a mounted volume, git will fail because the mount already exists. Always clone to a fresh path.

Don't Hand-Roll

Problem Don't Build Use Instead Why
SSH key injection Custom init script to copy keys Docker volume mounts Docker handles file ownership, permissions, and read-only enforcement at the kernel level. A script would need root, has race conditions, and can't enforce read-only at the syscall level.
Repo workspace management git clone at session start Docker volume mounts Mounting preserves host-side worktrees, branches, uncommitted changes, and avoids re-downloading. Clones are lost on container restart (5-min idle timeout).
Host key verification ssh-keyscan on every clone Mount existing known_hosts known_hosts already contains bitbucket.org keys on this host. A script-based approach would require network access and creates TOCTOU issues.
Env var forwarding Custom env injection in init script docker_forward_env Hermes-native mechanism. No script modification needed to add new vars.

Key insight: Docker volume mounts are the right tool for injecting files and directories into containers. Any solution that copies, symlinks, or clones data is strictly worse — it's slower, less secure, and more fragile. This phase should use zero copying: everything the container needs is either already in the image (git, ssh) or mounted from the host (keys, repos, script).

Runtime State Inventory

Not applicable — this is a greenfield configuration phase. No existing runtime state references old names, keys, or paths.

Category Items Found Action Required
Stored data None
Live service config None
OS-registered state None
Secrets/env vars None
Build artifacts None

Nothing found: This phase creates new config entries and one new script. It does not rename, refactor, or migrate anything.

Common Pitfalls

Pitfall 1: Read-Only Parent Volume Blocks Subpath Mounts

What goes wrong: Adding repo mount entries like ~/Razer/rai-ops:/workspace/rai-ops:rw fails silently or at container start with a filesystem error.

Why it happens: Docker subpath volume mounts require the parent path to be writable to create the mount point. If docker_mount_cwd_to_workspace mounts /workspace as :ro, Docker cannot create /workspace/<repo> directories.

How to avoid: Verify that docker_mount_cwd_to_workspace: true mounts /workspace as :rw (read-write). [VERIFIED: Tested :rw parent works, :ro fails.]

Warning signs: Container fails to start after adding repo volume entries. docker logs shows mountpoint ... read-only file system error.

Pitfall 2: SSH Host Key Verification Prompts Block Non-Interactive Git

What goes wrong: Agent runs git clone git@bitbucket.org:razersw/<repo>.git and the command hangs waiting for host key confirmation.

Why it happens: The container has no ~/.ssh/known_hosts with bitbucket.org's host key. SSH prompts "Are you sure you want to continue connecting?" and since the terminal is non-interactive, the connection hangs or fails.

How to avoid: Mount ~/.ssh/known_hosts:/root/.ssh/known_hosts:ro. The host already has bitbucket.org's host keys. [VERIFIED: grep shows 3 host keys for bitbucket.org in known_hosts; SSH auth test succeeded with known_hosts mounted.]

Warning signs: git clone hangs or returns Host key verification failed error.

Pitfall 3: Mounted SSH Key Permissions Cause SSH Rejection

What goes wrong: SSH refuses to use a mounted key, showing Permissions 0777 for '/root/.ssh/id_ed25519razer' are too open or similar.

Why it happens: On macOS, file permissions can be loose. When mounted into Docker, the permissions are preserved. SSH requires private keys to have strict permissions (group/other must not have any access).

How to avoid: Verify on host: stat -f "%Sp" ~/.ssh/id_ed25519razer should show -rwx------ or -rw-------. If permissions are wrong, fix with chmod 600 ~/.ssh/id_ed25519razer. [VERIFIED: Host keys are -rwx------ (700), which SSH accepts.]

Warning signs: ssh -T git@bitbucket.org inside container fails with bad permissions error.

Pitfall 4: On-Demand Clone Conflicts with Mounted Repo

What goes wrong: Agent runs git clone git@bitbucket.org:razersw/rai-ops.git /workspace/rai-ops but the path already exists (as a mounted volume). Git refuses to clone into a non-empty directory.

Why it happens: The default repos are already mounted at /workspace/<name>. If the agent tries to clone into the same path, git errors.

How to avoid: On-demand clones should use distinct paths. The agent should clone to /workspace/<name> only for repos NOT in DEFAULT_REPOS. For DEFAULT_REPOS, the mounted volume already serves as the workspace. [ASSUMED: Standard git behavior; no verification needed.]

Code Examples

Example 1: session-init.sh

#!/bin/bash
# session-init.sh — Verify DEFAULT_REPOS mounts at session start
# Runs via shell_init_files before agent prompt. Non-blocking.
# Reads DEFAULT_REPOS from environment (forwarded via docker_forward_env).
set -uo pipefail

DEFAULT_REPOS="${DEFAULT_REPOS:-}"

if [ -z "$DEFAULT_REPOS" ]; then
  echo "[session-init] DEFAULT_REPOS not set — skipping verification"
  exit 0
fi

# Split comma-separated list
IFS=',' read -ra REPOS <<< "$DEFAULT_REPOS"
ALL_OK=true

for repo in "${REPOS[@]}"; do
  # Trim whitespace
  repo="${repo#"${repo%%[![:space:]]*}"}"
  repo="${repo%"${repo##*[![:space:]]}"}"

  if [ -d "/workspace/$repo/.git" ]; then
    echo "[session-init] ✓ $repo — mounted at /workspace/$repo"
  else
    echo "[session-init] ⚠ $repo — NOT FOUND at /workspace/$repo"
    ALL_OK=false
  fi
done

if [ "$ALL_OK" = true ]; then
  echo "[session-init] All DEFAULT_REPOS verified"
else
  echo "[session-init] Some repos missing — check docker_volumes in config.yaml"
fi

exit 0  # always exit cleanly — non-blocking

Example 2: config.yaml Changes (Append to Existing)

terminal.docker_volumes — Append these entries:

terminal:
  docker_volumes:
    # ... existing entries (SSO cache, scripts) remain unchanged ...
    # SSH key mounts (read-only)
    - /Users/bapung/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro
    - /Users/bapung/.ssh/id_rsa:/root/.ssh/id_rsa:ro
    - /Users/bapung/.ssh/config:/root/.ssh/config:ro
    - /Users/bapung/.ssh/known_hosts:/root/.ssh/known_hosts:ro
    # Repo mounts (read-write — git operations)
    - /Users/bapung/Razer/rai-ops:/workspace/rai-ops:rw
    - /Users/bapung/Razer/rai-deployment:/workspace/rai-deployment:rw
    - /Users/bapung/Razer/rai-devtools:/workspace/rai-devtools:rw

terminal.shell_init_files — Set to:

terminal:
  shell_init_files:
    - /usr/local/bin/session-init.sh

terminal.docker_forward_env — Append DEFAULT_REPOS:

terminal:
  docker_forward_env:
    - JIRA_EMAIL
    - JIRA_API_TOKEN
    - DEFAULT_REPOS     # ← append this

Example 3: .env Addition

# DEFAULT_REPOS — repos mounted into every session workspace
# Comma-separated list. Each repo must have a matching docker_volume entry.
DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools

Example 4: On-Demand Clone (Agent Instruction)

# Inside Docker container:
# Clone an additional repo not in DEFAULT_REPOS:
git clone git@bitbucket.org:razersw/rai-somerepo.git /workspace/rai-somerepo

# For temporary work (ephemeral — lost on container restart):
git clone --depth 1 git@bitbucket.org:razersw/rai-somerepo.git /workspace/tmp/somerepo

State of the Art

Old Approach Current Approach When Changed Impact
(None) Manual repo cloning per session Auto-mounted repos via Docker volumes This phase Eliminates #1 UX complaint. Repos survive container restarts because they're host-mounted.
(None) SSH agent forwarding Mounted SSH keys (:ro) This phase More reliable (no socket dependency), survives restarts, read-only enforced by Docker.
(None) Full .ssh/ directory mount Per-file key mounts This phase Reduces credential exposure. Only specific keys are available to the agent.

Deprecated/outdated:

  • ~/.ssh:ro full directory mount: Replaced by per-file mounts. Full directory exposes all SSH keys (including any from GitHub, GitLab, etc.) to the agent. Mount only the keys the agent needs.

Assumptions Log

# Claim Section Risk if Wrong
A1 docker_mount_cwd_to_workspace: true mounts /workspace as :rw (not :ro) Architecture Patterns — Pattern 1 If Hermes mounts as :ro, subpath repo mounts will fail with "read-only file system" error. Mitigation: Planner adds a verification step to check the mount mode.
A2 shell_init_files runs the script at every container start, not just first start Architecture Patterns — Pattern 3 If it runs only on first container creation, the verification won't happen on container restart. Mitigation: Low risk — container restarts are infrequent (5-min idle timeout). The mounts are still present regardless of verification output.
A3 worktree: true in config doesn't interfere with docker_volumes subpath mounts Architecture Patterns If worktree mode creates separate namespaces or chroots, subpath mounts might resolve incorrectly. Mitigation: Hermes already has both worktree: true and docker_mount_cwd_to_workspace: true set simultaneously in current config, suggesting compatibility.
A4 The bitbucket.org host key in known_hosts will remain valid throughout v1.1 lifecycle Common Pitfalls — Pitfall 2 If Bitbucket rotates their host key, SSH connections will fail with "HOST KEY IDENTIFICATION CHANGED" error. Mitigation: Add StrictHostKeyChecking=accept-new to the SSH config entry for bitbucket.org as a belt-and-suspenders approach.

Open Questions

  1. Does docker_mount_cwd_to_workspace: true mount as :rw or :ro?

    • What we know: Standard Hermes behavior mounts the project directory (where Hermes was launched) to /workspace in the container. The agent creates files in /workspace/ during normal operation, implying :rw.
    • What's unclear: The exact mount mode Hermes uses. We haven't verified by inspecting the running container's mount table.
    • Recommendation: The planner should add a verification step: run a Hermes container, check mount | grep /workspace to confirm mode. If :ro, adjust the plan to either (a) change the CWD mount mode or (b) use a different base path for repo mounts.
  2. Does shell_init_files re-execute on container reconnect (after idle timeout)?

    • What we know: PITFALLS.md says it runs "before the shell prompt appears." Hermes containers use container_persistent: true with lifetime_seconds: 300 — containers survive for 5 min idle, then get destroyed.
    • What's unclear: On container reconnect (within the 5-min window), does shell_init_files re-execute? Or only on initial container creation?
    • Recommendation: Assume it runs on initial start only. The verification script is informational only — mounts are set up by Docker regardless.
  3. How to add/remove repos in the future?

    • What we know: CONTEXT.md says "User can add/remove repos by editing .env + adding/removing volume mounts." This requires editing config.yaml (docker_volumes), not just .env.
    • What's unclear: Should there be a helper script to automate this? e.g., ngn-add-repo <name> that updates both config.yaml and .env?
    • Recommendation: Defer — not needed for this phase. Document the manual process in a comment.

Environment Availability

Dependency Required By Available Version Fallback
Docker Volume mounts, SSH test Docker Desktop
Git On-demand clones 2.47.3 (in container)
OpenSSH SSH auth for git 10.0p2 (in container)
Hermes Config changes, script placement v0.16.x

Missing dependencies with no fallback: None

Missing dependencies with fallback: None

Security Domain

Required: security_enforcement is absent from .planning/config.json (absent = enabled).

Applicable ASVS Categories

ASVS Category Applies Standard Control
V2 Authentication yes SSH key-based auth to Bitbucket; keys mounted :ro
V4 Access Control yes Only id_ed25519razer and id_rsa mounted — limits credential exposure
V5 Input Validation no No user input processed at this layer
V6 Cryptography yes SSH private keys are cryptographic material; handled by OpenSSH inside container

Known Threat Patterns

Pattern STRIDE Standard Mitigation
SSH key exfiltration via agent prompt injection Information Disclosure Keys mounted :ro — agent can read but not modify. Per-file mount reduces exposure vs. full ~/.ssh/ directory.
Container breakout via Docker volume mounts Elevation of Privilege Repos mounted :rw — agent can modify repo files. This is intentional (git operations). Keys mounted :ro — cannot be tampered with.
Unauthorized Bitbucket access via stolen key Spoofing Key is the user's personal SSH key. Deferred: per-repo deploy keys for finer-grained access control.

Security Properties Verified

  • SSH keys are mounted :ro — agent cannot modify them, even as root inside the container. Docker enforces this at the VFS layer.
  • known_hosts is mounted — prevents MitM via host key spoofing (ensure the file is from a trusted source).
  • Key permissions are correct600/700 on host, preserved inside container.

Sources

Primary (HIGH confidence)

  • Docker volume mount tests — Verified end-to-end flow:
    • SSH key mounts + git auth: authenticated via ssh key [VERIFIED: Docker Test]
    • Subpath volume mounts with :rw parent: repo visible and writable [VERIFIED: Docker Test]
    • On-demand git clone: succeeded without errors [VERIFIED: Docker Test]
  • Host SSH config~/.ssh/config maps bitbucket.org → id_ed25519razer with IdentitiesOnly yes [VERIFIED: file read]
  • SSH keysid_ed25519razer and id_rsa exist with correct permissions [VERIFIED: ls -la]
  • Known hosts — bitbucket.org host keys present [VERIFIED: grep bitbucket.org ~/.ssh/known_hosts]
  • Repo remotes — All three repos have origin pointing to git@bitbucket.org:razersw/*.git [VERIFIED: git remote -v]
  • Hermes config — Existing docker_volumes, shell_init_files, docker_forward_env patterns [VERIFIED: file read]
  • Existing scriptsngn-jira, ngn-bitbucket, ngn-confluence use set -euo pipefail pattern [VERIFIED: file read]

Secondary (MEDIUM confidence)

  • PITFALLS.md section on blocking init scripts, SSH key exposure [CITED: .planning/research/PITFALLS.md]
  • REQUIREMENTS.md §REPO-01, REPO-02 — Requirement definitions [CITED: .planning/REQUIREMENTS.md]

Tertiary (LOW confidence)

  • Hermes worktree: true interaction with docker_volumes — Not tested. Assumed compatible based on both being set simultaneously in current config. [ASSUMED: A3]

Metadata

Confidence breakdown:

  • Standard stack: HIGH — All tools verified present in Docker image
  • Architecture: HIGH — All patterns tested end-to-end with live Docker containers
  • Pitfalls: HIGH — Pitfall 1 verified experimentally (:ro parent fails), Pitfall 2 verified (known_hosts required), Pitfalls 3-4 are well-known SSH/git behaviors

Research date: 2026-06-14 Valid until: 2026-07-14 (30 days — Hermes and Docker config patterns are stable)