Once a target is in Conning, it knows everything ssh needs: the host, port and user, the key or the password in your keychain, the pinned host key and the jump hosts on the way. So Eran asked for the obvious thing: conn ssh lab, a shell there with no second copy of those settings in ~/.ssh/config (D58).
It is plain ssh -t with the config Conning already writes for every call, so the shell gets the same locked-down connection as a command: the host key is checked against Conning's own known_hosts, forwarding is off, a password goes through the askpass helper and never onto a command line, and a bastion is crossed hop by hop. Nothing is read from your ssh config.
It is for you, not the agent. It refuses without a terminal, it isn't one of the four MCP tools, and conn mcp install adds it to the agent's deny rules next to raw ssh. Inside the shell there is no policy check and no redaction, because you are the one typing. The audit log still records the shell opening and closing, so conn watch shows it. The end-to-end suite opens a shell on its real sshd, directly and through a jump host, types a command and checks the exit code.
Trying it out, Eran also wanted conn targets to say where each target is. It used to hide the host on purpose (D41), but a host is no more a secret than the user it logs in as, and the agent still can't go around Conning to reach it. So the listing now shows host=10.0.0.5, with the port when it isn't 22, for you and the agent alike (D59).
#21 · A skill for the agent 2026-10-19
The MCP server tells the agent a few lines about Conning on every turn, and that is all it can afford: anything longer costs context whether or not the task touches a remote machine. So the habits that make Conning pleasant to drive had nowhere to live. Start a build with wait=0 and follow it with read, ask for grep before a bigger max_bytes, write PowerShell for a Windows target, and when a command is pending, tell the human why and wait instead of trying a variant.
They now live in a skill. conn mcp install claude (or codex, cursor, opencode, pi) writes conning/SKILL.md into that client's skills folder next to the server entry and the deny rules. The agent sees one line describing it and reads the rest only when a task needs a remote machine (D57). The skill ships inside the package, so it matches the version you run, and running install again after an upgrade refreshes it. It covers the four tools only; the human-only commands stay out of it.
#20 · Windows runs that never started 2026-10-17
Eran ran conn run on a Windows target and it sat there until he pressed Ctrl-C, which then printed a page of traceback. Conning sends its scripts to PowerShell over stdin, so secrets never sit on a command line, and the loader read stdin to the end. Windows PowerShell behind Windows OpenSSH never sees that end. Two small tests on his machine showed it: reading to the end hung, reading line by line worked. So every script now ends with a marker line and the loader stops there. Ctrl-C now prints interrupted and exits at once, and a keychain read stuck on an unseen macOS dialog gives up after 30 seconds and says so.
The first working dir showed two more things. It listed Conning's own jobs folder, because WMI starts the job there, so Windows jobs now start in your home folder like they do over ssh. And that folder was full of every job's script, output, pid and exit code, which nothing ever removed, on Linux too. Once a job has ended and its output is copied home, those files are now deleted from the target (D56). The local log keeps the output.
#19 · Live output for humans 2026-10-17
Eran has been using Conning a lot, and found that conn run doesn't show interactive output well: loading animations, spinners and progress bars. That is by design for an agent. A command runs as a detached job with no terminal, and Conning polls its log, so the agent gets a capped result that leads with the exit code and survives a dropped connection. A spinner in that log is just a pile of carriage returns that arrives at the end.
A human watching wants what ssh -t gives. So conn run -t lab apt upgrade (or --tty) runs the command in a remote pty with your terminal as its stdin: the output appears as it happens, colours and progress bars draw in place, and you can answer a prompt or press Ctrl-C (D55). Everything else stays the same. The command goes through the same policy, and one that would wait for approval asks you right there first. The output is still redacted as it streams; the redactor holds back only a tail that could be the start of a secret, so a spinner never waits. Injected env values travel in a file that deletes itself as it starts, never on the command line.
It refuses without a terminal, so an agent's shell gets an error instead of a pty, and the four MCP tools don't change at all. It runs on one Linux or macOS target, and --wait, --max-bytes and --grep don't apply, since you are watching the whole thing. The end-to-end suite runs it against a real sshd, typing an answer into a prompt.
#18 · Tidy commands, and a switch for auto 2026-10-17
Eran asked an agent how to move a target from ask to auto. The agent got it right, and that was the problem: the answer was to open targets.toml and add mode = "auto" by hand, or try conn add --mode auto --retry, which nobody had tested. Switching a mode is a normal thing to want, so it gets a command: conn targets mode lab auto (D54). With no mode it prints the current one, and it takes a comma list or tag: like allow does.
It follows the same rule as the allowlist. Turning auto on grants trust, so it needs you at a terminal and a y/N, and an agent calling it from its shell gets "needs a human". Going back to ask only takes trust away, so it works from anywhere, the way deny does. Both land in the audit log, and conn mcp install adds the command to the agent's deny rules.
Eran also asked whether the help had grown redundant. It had. conn ls was conn targets again, and conn add --retry did exactly what conn approve <target> does. So everything that manages a target now sits under conn targets: add, import, test, mode, rename and rm. Plain conn targets still lists, exactly as the agent's tool does, so nothing changes for an agent. approve, allow and deny stay at the top, because every approval message already says conn approve a:<id>. The old spellings simply go away, with no aliases to carry. conn -h now reads in four sections: agent commands, targets, trust, and this machine. It ships as 0.6.1.
#17 · Two modes: ask and auto 2026-10-17
Eran asked to simplify how targets grant permission. There were three modes: observe, ask and lease. Anyone who uses a coding agent already knows the two that matter, ask or auto-approve, so those are the two Conning has now (D52).
Reading the code made the call easy. Observe refused anything off the read-only list and the allow rules, but ask with that same list already lets reads through and holds everything else for you, so you stay the wall. Lease was odder: mode = "lease" in the target file did nothing at all, and only the conn lease timer did. Both modes are gone, along with the timer, its file and the command.
auto runs anything without asking until you set the mode back to ask. It gives up the timer in exchange for one less thing to remember, so conn doctor warns about every target in auto. A target file that still says observe or lease stays fail-closed: it fails to load, and the error prints the conn add line that recreates that target, with observe becoming ask and lease becoming auto. The change removes about 40 lines of source and one command.
#16 · On the map: the MCP Registry 2026-10-17
With jump hosts in, the only thing left on the roadmap is people using Conning, and the first place they look is the official MCP Registry, which clients and other directories read their server lists from. Back at D31 we said "not yet". Now it is yes (D51).
The registry holds only metadata, so the listing is one file, server.json, naming both ways to run Conning: uvx conning mcp from PyPI, and the GHCR image with --network host, your config folder as its volume and your ssh-agent socket. The registry checks that we own both: the PyPI page carries a hidden mcp-name comment from the README, and the image carries a matching label. The release workflow now publishes the listing as its last step, after the package and the image are out, logging in with the repository's own GitHub identity, so there is no token to store and the listing moves with every release. The first listing ships with 0.6.0.
#15 · Counting tokens 2026-10-17
"Fewer tools beat more tools" has been a principle since day one, but nobody had measured it. So we ran Conning and three popular SSH MCP servers over real MCP stdio against the same throwaway sshd, sent the same commands, and counted what each one puts in the agent's context. These are characters on the wire; the token figures assume about 3.5 characters a token.
The first cost is the one every turn pays: tool schemas plus server instructions, before the agent runs anything.
| Server | Tools | Chars per turn | ≈ tokens |
|---|---|---|---|
| Conning | 4 | 3,007 (was 3,605) | ~0.9k |
| ssh-mcp-server | 10 | 7,054 | ~2.0k |
| ssh-mcp | 14 | 10,298 | ~2.9k |
| mcp-ssh-manager | 37 | 41,563 | ~11.9k |
That's the number to be proud of: a third of the next smallest, and a fourteenth of the largest. Big output is a different story. On 1.3 MB of seq output, two of the others hand back over a megabyte. Conning returns about 600 characters: the first 20 lines, the last 60 and a handle. Cutting isn't the clever part, though. What matters is that the agent controls it: it can raise max_bytes up to 16 KB, filter with grep, or page any range of the full output later, without running the command again.
The measurement also showed where Conning lost. Every result started with a header like exit=0 dur=0.6s out=o:be1a0d5e. The others print bare output, so echo hello cost 36 characters against 6. Worse, the tools declared an output schema that only said "returns a string", so the MCP SDK sent every result twice, once as text and again as structuredContent. Both are fixed (D50). Results are text only, which also takes four useless blocks out of the schemas. The header drops dur= under a second, and names the handle only when something is hidden, either cut or filtered by grep. echo hello now returns exit=0 and hello, 12 characters, still exit code first. The test that caps the schema size went from 6,000 characters to 2,600, so the savings can't quietly grow back.
#14 · Through the bastion: jump hosts 2026-10-16
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.