Eran asked whether Conning reaches machines behind a jump box. It didn't. D7 said "keep ProxyJump", but Conning never reads ~/.ssh/config, and the config it writes for each target had no ProxyJump in it. So a lab behind a bastion was out of reach, and conn import quietly dropped the line.
Now a target can name another target as its jump host: conn add lab --host 10.0.0.5 --user tester --jump gw, or jump = "gw" in the target file, and chains work too. Making the bastion a target, rather than a string passed to ssh, is the point (D49). Every hop gets the same treatment as a target: its own block in the generated config, pinned to Conning's own known_hosts, forwarding off, its own login from a secret reference. ssh runs each hop with that same file, so nothing comes from your ssh config. When two hops both log in with a password, the askpass helper reads which host ssh is asking about from the prompt and answers with that hop's password, and with nothing for a host it doesn't know.
Pinning needed a new trick, because ssh-keyscan can't jump. When you approve a target behind a bastion, ssh itself connects through the pinned hops, records the key the target offers into a scratch file and logs in with no credential at all. You confirm the fingerprint as usual, and only then does Conning send a password. The end-to-end suite uses its own sshd as the bastion, reaching itself again as localhost. It checks that each hop gets its own password, and that a changed bastion key closes the way through. It also found that ssh hides a jump host's own errors behind a shared connection, so a bare failure behind a jump now says to conn test the jump host.
#13 · A real target in CI, and the bug it caught 2026-10-16
Every test so far ran against a fake SSH, which proves Conning's own logic and nothing about what OpenSSH does with the configs Conning writes. Eran pointed out the obvious: a real target costs almost nothing, because an SSH server can run on the CI machine itself. So every push now starts a throwaway sshd on 127.0.0.1 with a test user, a key and a random password, and drives the real conn against it as two kinds of caller. As an agent, each command runs with no terminal. As a human, the human-only commands run in a real pseudo-terminal and get their answers typed when asked: a password into conn add, once, always or deny at an approval, a yes to a lease. Then Eran asked for all of it, so the suite now walks every command in conn help and nearly every option, plus the MCP server over stdio, approving inside the client, and conn mcp install for all five clients, after which it starts the server exactly as the client would. One more test reads conn help and fails if a command has no end-to-end test, so new features can't skip it (D48).
The first run found a real bug. ssh shares one connection per host, port and user, so a second target for the same login quietly rode the first one's connection for up to ten minutes, skipping its own key, password and pinned host key. A target with a wrong password still ran commands. Each target now gets its own connection, named by a hash of its generated config. Mocks could never have caught that, which is the whole argument for the new job.
#12 · A new name, same machine 2026-10-16
Using Conning in the field, Eran found that the names picked on day one stop fitting. Until now the only way out was conn rm and conn add, which threw away the pinned host keys, the allow rules and any lease, and left the stored password under the old name. conn rename <old> <new> carries all of it along: the target's rules, its pinned keys, the keychain password conn add stored, a lease, commands still waiting for approval and jobs still running. The password moves first, and if that fails nothing changes; it moves rather than copies, so a later conn add <old> can't overwrite it. A secret reference you chose yourself stays as it is. History keeps the old name: finished jobs, answered approvals and the audit log, which gets a renamed entry. A rename changes what rules and approvals point at, so it is human-only: it needs a terminal, and conn mcp install denies it to the agent (D47).
#11 · In a box: the server as a container 2026-10-15
Eran asked for the Docker image next, and the image is the easy part. What took thinking is that a container has neither of the two things Conning leans on: the OS keychain, and a terminal to approve at. Rather than tunnel both in, we split the product along the line it already had. The container runs the half that runs commands: Python, the OpenSSH client, Conning, and one volume at /conning holding targets, pinned host keys and job output. The human keeps the other half on their own machine: conn approve, add, lease and allow run from the host against the same folder, so nothing inside the container can grant trust to itself, and approving in the agent client works as before. Secrets follow the same rule: a keychain: reference now fails inside a container with a sentence naming what does work there, which is env:, op:// or a forwarded ssh-agent, and conn doctor says the same before anything breaks. Lab networks are the one honest rough edge: on Linux --network host reaches them, and elsewhere the container needs a route of its own (D46). CI now builds the image on every pull request and runs conn doctor inside it; a tag pushes amd64 and arm64 to GHCR.
#10 · Ponytail: a smaller codebase, and opencode and pi 2026-10-15
With M2 working, Eran asked what we had overbuilt, without dropping a feature. We measured first: 2,592 lines of code in src, not counting blank lines, comments, docstrings or tests. The fixes were plain. The conn command line had a two-to-five line wrapper per command; it is now one table of one-line handlers. Long word lists, such as the commands that can run anything and the flags the read-only list accepts, were one word per line and are now one string each. The target file's list of allowed keys now comes from the Target fields, so the two can't drift. Small pieces of copied logic got one home: picking a live target, running a short script on either OS, asking yes or no, turning a command into the words policy checks. Dead code went: a function only tests called, a transport method whose answer was always rc == 255, and two fields nothing read. That left 2,407 lines. Then two clients joined conn mcp install. opencode reads MCP servers and permission rules from one JSON file. pi supports MCP servers now, but it has no permission rules, so installing there adds the server and says the agent can still run ssh itself (D45). The total after both is 2,435. Removing whole features is a separate conversation, and the candidates are listed in the PR.
#9 · M2's last two pieces: Windows jobs and one-line setup 2026-10-15
Windows targets used to run each command while the ssh connection stayed open, because Windows OpenSSH ends every process a session started when that session closes. Now a Windows job starts the way a Linux one does. Its wrapper is created through WMI, which puts it outside the ssh session. The wrapper runs the command with stdout and stderr in one file, kills the whole process tree on a timeout or a --signal, and writes the exit code last, so read polls it by byte offset like any other job (D43). Secrets got a fix on the way. The scripts used to travel inside -EncodedCommand, which is a command line any process listing on the machine can show. Now that flag only carries a tiny loader, and the script, injected values included, arrives over stdin. The job's own copy of those values sits in a file only the user can read, which the wrapper deletes the moment it starts. The tests run the real PowerShell scripts against a local pwsh, and on the Windows CI runner that includes the WMI launch itself. The second piece is conn mcp install. For Claude Code, Codex or Cursor, it adds the server and writes deny rules in that client's own format: no raw ssh, scp or sftp, no conn approve, lease or allow, and no reading Conning's config. It merges with what is already in those files, keeps a backup, and changes nothing on a second run (D44). Those rules match the command the agent types, so /usr/bin/ssh would slip past them. They are a guardrail, and the wall is still Conning's four tools.
#8 · First real run: prompts, users and PATH 2026-10-15
Eran's first command on a real machine after M2 merged was a test CLI's status, which isn't on any list, so Conning answered with status=pending_approval id=a:... and a conn approve line to copy. That answer is right for an agent and wrong for a person sitting at the terminal. Now conn run asks right there when it has a terminal (once, always, deny) and prints the result in the same go, with no desktop pop-up. Agents have no terminal, so they still get the id. He had also typed a second machine name before the command, because the CLI's own error hint used lab-lin1 as its example target. The hint now uses the target you typed. And a target name in the command's spot is an error that shows the comma form, lab,lab-lin1, instead of being sent to the machine as a program (D40). Then two more from the same session. conn targets didn't say which user each machine logs in as. It does now, since the user isn't a secret; the host still never shows. A target stays one login, so working as another user means adding the machine again with conn add lab-tester --like lab --user tester, which copies the host, port, OS and tags and asks for that user's password (D41). And his test tool came back not found as root. A job runs in a non-interactive shell that skips the login profile, so anything the profile adds to PATH, like ~/.local/bin, was missing. Jobs now take the PATH a login shell would have and drop the rest of the profile's output (D42).
#7 · M2: approvals where you already are 2026-10-15
M1 merged, and M2 started the same morning. A command that needs approval can now be answered in three places. The agent client asks you before the command is queued: once, always, or deny. That works on both MCP protocol versions, through the SDK's resolver: newer clients get the question back as an input_required result and retry the call with your answer, older ones get it mid-call. Claude first made it opt-in, since the protocol lets a client answer a prompt by itself. Eran turned it on by default: Conning is a general MCP for every agent, and clients without prompts fall back to the other two places (D38). The second place is conn watch, a terminal that prints every command, decision and job exit as it happens, with new output under it, and prompts for anything waiting (D36). The third is conn approve, as before, now with a desktop notification. All three go through one function, so they remember the same rule and leave the same audit entry. That audit log is hash-chained, and conn doctor says if a line was edited (D34). Eran also said yes to a built-in list of safe read-only commands, so uname, df, ps, ip addr or systemctl status run on a new machine without asking. Each one has its own argument check, because a word prefix isn't enough: hostname prints the name but hostname x sets it, and ip addr add changes the network (D39). conn allow and conn deny manage other rules by hand (D35), and conn doctor checks everything Eran's first test tripped over (D37). There are 280 tests at 100% coverage, and a replay against a real sshd checked each answer on both protocol versions.
#6 · First real machine 2026-10-15
Eran ran the M1 branch on his macOS workstation against a lab VM, and it worked: approve the target, run uname, approve the command, read the output. Getting there found real bugs. On macOS the SSH control socket path was too long, so it now lives in a short private /tmp/conning-<uid> and is skipped if it still won't fit. His local agent then wrote up what got in its way. conn run lab uname -a failed because the parser rejected flags after the target, so conn now hands everything after its own options to the remote command (D29). A bad secret printed a traceback; now every error is one line, in the CLI and in all four tools. Running wrote to the config dir, which a sandbox mounts read-only, so output and approvals moved to a state dir (D28). An empty conn ls now says which file it read, and a pending target says why it is still pending. Approving a command now prints its result, without hiding it from the agent. Eran asked for a clean-clone test guide; docs/manual-test.md walks through every tool, and Claude replayed it end to end against a local sshd. There are now 152 tests at 100% coverage. A default read-only allowlist, conn allow, conn doctor and a Docker image are filed as issues for later milestones.
#5 · M1: commands over SSH 2026-10-14
The four tools now work against real machines. run starts a detached job and returns the exit code first, or a job handle once wait runs out. read streams new lines, sends signals, pages saved output and reads remote files. transfer copies with scp and checks the sha256. On the human side, conn add, approve, import, test, rm and lease work, and host keys are pinned when you approve a target. Secrets are redacted from all output, along with their base64, URL-encoded and hex forms. It was checked end to end against a local sshd (D24 to D27). Eran then asked for unit tests with mocks and 95%+ coverage. There are now 138 tests at 100% coverage, and CI fails below 95%. Writing them found four bugs, all fixed.
#4 · The repo and the M0 scaffold 2026-10-14
The repo landed on GitHub, not GitLab: EranYonai/Conning, Apache-2.0 for its patent grant. The first draft PR scaffolds v0.0.1. It adds the conn CLI, an MCP server with exactly four tools, a fail-closed target file, the output cap, and two-word allowlist rules. Tests keep the README's CLI help in sync with the code, and another caps the size of the tool schemas, about 3 KB today. CI runs on Linux, Windows and macOS, and PyPI releases go through Trusted Publishing. Running commands over SSH is M1.
#3 · Walkthrough, part two 2026-10-14
Tools locked at four, with plain text by default (D5, D17). Eran turned the 8 KB cap into an argument the agent can raise (D14) and asked for progress and async. Claude explained that the model only sees a tool call when it returns, so progress streams to the human while the agent long-polls for new lines (D15). Async came from wait=0 (D16), with no new tool. New lab targets start in ask (D8). Eran asked what "always allow" remembers, then set it to the first two words: "You see why?" The verb is where the danger lives (D18). Transport is SSH-only, and Windows hosts without OpenSSH get a fix the agent relays to the user (D19). Passwords are first-class (D20).
#2 · Walkthrough, part one 2026-10-14
Eran asked for a section-by-section walkthrough to lock the design. Answers so far: agents may use the CLI from bash (D3); single developer on their own machine, no team file (D4); the name is Conning (D11); cache the CLI help in the README (D10). Eran wants the agent able to enrol targets. Claude proposed "agent proposes, human confirms" (D9) because one injected log line could otherwise become a live host. Eran also asked for this log.
#1 · Idea, debate, design page 2026-10-14
Eran's idea: a control room MCP that vaults credentials for remote test machines and lets a developer's own agent run commands there. Four sub-agents (product, security, protocol, contrarian) wrote positions and then rebutted each other. A fifth surveyed open source: SSH MCP servers, credential brokers, Ansible and pyinfra, WinRM libraries, and agent sandboxes. By round two the four had converged on the shape in this log. A registry check retired Patchbay and Conning took its place. The first design page was published with mocks of onboarding, the agent's view, the watch view and the target file.