Shell integration

Rendered from docs/shell-integration.md in the repository.

Instead of only invoking ssh/tmux/docker on your behalf, agent-container can emit shell-evaluable configuration — a command line to run, or environment assignments to set — so you can eval $(agent-container …), alias it, script it, or drop it into ~/.ssh/config. Modeled on limactl show-ssh, eval $(minikube docker-env), and docker context. Executing directly stays the default where it already existed (notably attach); this is host-side only — nothing about the container changes.

agent-container attach acme --print
# -> ssh dev@localhost -p 2206 -t tmux attach -t main
eval "$(agent-container attach acme --print)"     # or run/alias the line yourself

The printed command is byte-for-byte what attach would execute (they render from one definition, so they can never drift). Running it in your shell means your own ssh-agent / smartcard / known_hosts / ~/.ssh/config handle the connection — the invocation quirks the tool is hard-pressed to drive correctly in every environment just work in your terminal.

An SSH-config stanza — attach --ssh-config#

agent-container attach acme --ssh-config >> ~/.ssh/config
ssh acme        # attaches via the appended Host stanza

Emits a Host <name> block (HostName / User / Port / RequestTTY yes / RemoteCommand tmux attach -t main) so you connect by alias later — no hand-editing of addresses/ports/users.

Attach is verified (Feature 018)#

Both the printed command and the stanza carry UserKnownHostsFile (the tool's own file, never yours) and StrictHostKeyChecking=yes. Three outcomes:

State What happens
the pinned key matches connects, no prompt
the key differs ssh refuses. Never a prompt — a mismatch contradicts a claim the tool made itself
nothing pinned warns, prints the fingerprint, says accepting cannot detect a container that was replaced, and asks

A refusal means the container's identity changed without a deploy. Every deploy re-pins, so down --purge + up is silent; a mismatch is not something the tool did. Check whether you expected a rebuild before accepting anything.

Nothing pinned happens on a second machine (the pin lives on the one that deployed), or if you deleted the state dir. The better fix is agent-container list --json, which gives you the known_hosts line from the machine that deployed — that entry predates what it checks, where accepting at the prompt does not. --trust-unpinned accepts without asking, for unattended use, and says exactly as much as the prompt would have.

--print and --ssh-config never prompt — they connect to nothing — but they do warn when nothing is pinned, so you are not handed a command that refuses for a reason the output never mentioned.

Target a host with your own docker/podman — host env#

eval "$(agent-container host env hz1)"     # export DOCKER_CONTEXT=agent-container-hz1
docker ps                                   # lists that host's containers, no wrapper
eval "$(agent-container host env --unset)"  # revert

host env is print-only by nature: a child process cannot change your parent shell's environment (that is exactly why it exists to be eval'd).

Shell dialects — --shell posix|fish|pwsh#

Default posix (bash/zsh). Also fish (set -x / set -e) and PowerShell / pwsh ($env:NAME = '…' / Remove-Item Env:NAME). PowerShell has no eval $(…); the idiom is Invoke-Expression:

agent-container host env hz1 --shell pwsh | Invoke-Expression

Each dialect quotes with its own rules (POSIX via shlex; fish backslash-escaping; pwsh ''-doubling) so the emitted text is eval-safe against any name/path/ address — a value can never word-split, expand, or inject.

The eval contract (why eval $(…) is safe)#

Design note (extensibility)#

Each print-capable operation computes one structured ShellAction (env set/unset ops + command lines), then either renders it for a dialect or executes it. That "compute the action, then choose how to realize it" seam is deliberately backend-extensible: a future emitter (e.g. infrastructure-as-code) could realize the same action differently. No such backend is built in this feature — only the seam that admits one. See specs/005-shell-integration/.