Conning Dev Blog
Dev blog · since 2026-10-14

You have
the conn

Your agent stays on your laptop. Conning holds the keys to your test machines and lets the agent work there.

An open-source MCP server and CLI. The agent gets four lean tools to run commands and move files on remote targets. It never sees a password, and you approve anything risky.

4agent tools0secrets in context5sub-agent voices2debate rounds2,654lines of logic387tests
>_LAB-LIN1LAB-WIN1PERF · OBSERVE

The story so far

>_

Seven short chapters. Every post is in Posts.

1

The harness was in the wrong place

The day before, Eran had shipped a side project: a small pi-style agent living inside a test CLI on a lab VM. It worked. Getting there took a wheel build, a sync, a module load, a credentials wizard and a reverse SSH tunnel, because the lab firewall wouldn't let the VM reach the LLM.

Looking back, the redundancy was obvious. Every developer already has a capable agent on their own machine. Why rebuild one on every VM?

Instead of having the CLI on the local remote machine, developers usually prefer to work with their own setup and dev machine. … The interface is 100% agent to agent, so it needs to work in a good, efficient way.Eran, the first message
2

Five voices, told to argue

Eran asked for the plan to go to sub-agents and for them to fight about it. Four got the same brief with different jobs. A product lead owned the first five minutes. A security skeptic owned the threat model. A protocol architect owned every token. A contrarian argued, in the spirit of pi, that none of this needed MCP at all. A fifth agent went off to survey open source.

Product"The first five minutes are the product." Under three minutes from install to the first remote command.
Security"The server is the wall." Prompt injection from remote output, and the agent already has bash as you.
Protocol"Four tools, not fourteen." Exit code first, head and tail, a handle for the rest.
Contrarian"You don't need MCP; ssh exists." ssh config, ControlMaster, 1Password's ssh agent.
3

Every sub-agent argued

Round two was rebuttals: each sub-agent read the other three and argued back. That is where the design was decided, because every one of them changed its mind about something. Security dropped its encrypted vault file after the contrarian argued that a homemade vault is how projects end up with CVEs. The contrarian dropped "MCP later" once product pointed out that approvals and long jobs need a long-lived process anyway. Product gave up its fifth tool and its favourite word, "rig". Protocol dropped server-side shell sessions as hidden state.

What was left is small: a CLI and an MCP server on one core, four agent tools, secrets as references, and OpenSSH under a config Conning generates itself.

› run {"target":"tag:smoke","argv":["pytest","-q"],"wait":30} 2 targets: 1 ok, 1 running exit=0 lab-lin1 dur=21.4s (identical output collapsed) status=running lab-win1 job=j:7 out=o:3c2
4

Reading open source

Before any code, a fifth agent read the open-source tools in this space. None of them combined secrets kept out of the model, a token-lean exec API, and both Windows and Linux targets, but most had one good idea. We took "access without exposure" from 1Password and HashiCorp Boundary, deny-first policy from Teleport, the timeout that hands control back from OpenHands, and job handles with tail reads from Desktop Commander and E2B. One SSH MCP server spent about 43.5k tokens on its 37 tools, and that became our warning.

We still build our own, so Conning stays one small server you can read end to end, with few dependencies and nothing hidden.

5

Locking it in, one question at a time

Eran asked for a walkthrough instead of a document: a summary of each section, then the questions that close it. The first answers pulled the scope in. Conning is a tool for one developer on their own machine, so the team-shareable target file went away. Agents with only bash get the CLI as well as MCP. And the agent should know its own helpers well enough to enrol a new target.

That last answer pushed on a rule all four voices had agreed on: the agent can never add a target. The compromise is that the agent proposes the target, it waits as pending, and Eran confirms it with one keystroke. Eran also cut a round trip: cache the CLI help in the README, so the agent doesn't have to call --help every session.

The hardest question was how much of an approval to remember. When you press "always allow", Conning saves a rule. If the rule kept only the first word, allowing systemctl status would also allow systemctl stop. If it kept the whole line, every new flag would ask again. Eran chose two words: systemctl status runs freely from then on, and systemctl stop still asks. That side project had learned this the hard way, when its read-only mode let writes through the test CLI's subcommands.

6

First contact

M1 is where Conning started touching machines. It still shells out to plain OpenSSH, but never through your ~/.ssh/config. Each target gets a generated config: the host key pinned under its own alias, strict checking, every kind of forwarding off, and one reused connection so a burst of commands costs one handshake. A password never appears on a command line. ssh asks a tiny helper for it, and the helper reads it from the ssh process's environment.

The hard part was long jobs. A test run can outlive an SSH connection, so a command doesn't run inside the session. It runs detached on the target, in its own process group, writing to a file under ~/.conning/jobs. The agent's read asks for bytes from an offset, so it only ever sees new lines. A signal reaches the whole process tree. Old images without setsid or timeout still work, with less protection. The first version had a bug: TERM killed the wrapper before it could record the exit code. The fix keeps the wrapper alive and signals only the command's group.

Eran's rule for this milestone was blunt: mock as much as possible and keep coverage above 95%. The trick that made it honest is a fake transport that runs Conning's real remote scripts with the local sh against a throwaway home directory. The job logic gets tested for real without a server. Getting to 100% found four bugs a happy-path demo would have missed. A failed fan-out start was listed as a job handle. An agent could save a password target with no secret, which then broke the whole target file. Typing "e" on a command that has no rule to remember approved it anyway. A single giant line of output, such as minified JSON, crashed the output cap.

7

Six versions in three days

Each milestone became a version. The first two never left the repo; 0.2.0 was the first on PyPI. Lines of logic counts the Python in src, without blank lines, comments, docstrings or tests.

  1. 0.0.1Oct 14 · scaffold

    The conn CLI, an MCP server with the four tools, a fail-closed target file, the output cap and CI. Only targets worked.

    413 lines of logic
  2. >_
    0.1.0Oct 15 · commands over SSH

    Generated ssh configs with pinned host keys, detached jobs you read by offset, transfer with sha256 checks, approvals, leases and secret redaction.

    1,691 lines of logic
  3. 0.2.0Oct 15 · first on PyPIreleased

    Approve in the agent client or conn watch, a hash-chained audit log, conn doctor, allow/deny, a built-in read-only list, detached Windows jobs and conn mcp install for five clients. Then a trim from 2,592 lines without dropping a feature.

    2,435 lines of logic
  4. CONN
    0.3.0Oct 15 · in a boxreleased

    The server as a container image on GHCR, amd64 and arm64, with approving kept on the host.

    2,450 lines of logic
  5. >_
    0.4.0Oct 16 · a real target in CIreleased

    conn rename, end-to-end tests of every command against a real sshd on each push, and one ssh connection per target.

    2,520 lines of logic
  6. >_
    0.5.0Oct 16 · through the bastionreleased

    Jump hosts: a target reached through other targets, each hop with its own login and pinned host key, and conn import keeping ProxyJump.

    2,654 lines of logic

The goal hasn't moved: your own agent, on your own machine, working on a lab VM as easily as on localhost, without ever holding its keys.

The shape, today

********

Conning runs next to your agent. Everything the agent does goes through four tools. Nothing outside those four tools can reach a target, and the policy behind them lives in code, not in the prompt.

ToolSignatureBehavior
targetstargets(filter?, check?)One line per target: name, OS, shell, tags, mode, last seen. Never a host, a user or a secret.
runrun(target, cmd|argv, cwd?, wait=30, timeout=3600, max_bytes=8192, grep?)One target, a list, or tag:x. Exit code first, output capped at 8 KB. Past wait it returns a job handle and the command keeps running.
readread(id, wait?, range?, grep?, signal?)Long-polls a job for new lines (j:), slices saved output (o:), checks an approval (a:), or pages a remote file (target:path).
transfertransfer(source, dest)Copies files between your machine and a target over the open connection and checks the sha256. File contents never enter the model's context.

Decisions

NOPE

Every choice gets a number and a reason. 47 are in force; 5 were replaced and stay listed, struck through, so the trail is visible.

▸ Show all 52 decisions▾ Hide decisions
#DecisionWhyStatus
D39Every target allows a built-in read-only list (system info, processes, disks, network, service status, logs), each checked so its changing forms still ask; no file readers; safe = false opts outEran (#4): a new machine shouldn't need an approval for uname; the read tool already covers fileslocked
D40conn run at a terminal asks right there when a command needs approval, then prints the result; agents, with no terminal, still get the a:<id>. A target name where the command should start is an error that suggests a,bEran's first run on a real machine: copying an id into conn approve is clumsy, and conn run lab lab-lin1 ... looked like it needed a second namelocked
D41targets shows the user each target logs in as, never the host or a secret; conn add <name> --like <target> --user <u> adds the same machine as another userEran: "which user?" A target is one login, so another user is another targetlocked
D42Linux and macOS jobs take the PATH a login shell would have; the profile's other output is droppedA non-interactive ssh session skips the profile, so a tool in ~/.local/bin came back "not found"locked
D43Windows jobs run detached like POSIX ones: a wrapper created through WMI, outside the ssh session, merges output into one file, times out the whole process tree and is polled by byte offset. Scripts reach PowerShell over stdin, so secrets never sit on a command lineWindows OpenSSH ends everything a session started when it closes, and a test run has to survive a dropped linklocked
D44conn mcp install claude|codex|cursor adds the server and denies the agent raw ssh/scp/sftp, the human-only conn commands and reads of Conning's config, in each client's own rule format; it merges, keeps a backup, and a second run changes nothingOne command per client. The rules are a guardrail that matches what the agent types; the four tools stay the walllocked
D45conn mcp install also covers opencode and pi. opencode gets the server and deny rules, placed after any catch-all because its last matching rule wins; pi gets the server only, because it has no permission rulesBoth read MCP servers from a JSON file, so each one is a small additionlocked
D46The MCP server also ships as a container image (ghcr.io/eranyonai/conning, amd64 and arm64, pushed by the release workflow on a tag): a slim Python base plus the OpenSSH client, running conn as the owner of the shared folder, with CONNING_HOME=/conning and CONNING_STATE=/conning/state on one volume and conn mcp as the entrypoint. keychain: secrets are refused there, naming env:, op:// and ssh-agent forwarding instead; approving stays on the host, sharing the same folder; a lab LAN needs --network host or a route of its ownDocker is how many MCP clients install a server, and a container can reach neither the OS keychain nor a terminal, so the image carries only the running halflocked
D47conn rename <old> <new> renames a target and carries along everything kept under its name: its place in the target file and its rules, pinned host keys, the keychain password conn add stored (moved to the new name, so a later conn add <old> can't overwrite it; a reference the human chose stays as is), a lease, a pending target's problem, commands still waiting for approval and jobs still running. Finished jobs, answered approvals and the audit log keep the old name, plus a renamed entry. It needs a terminal, and conn mcp install denies it to the agentEran, from field use: names chosen on day one stop fitting, and rm plus add would lose pins, rules and the passwordlocked
D48End-to-end tests run the real conn and the MCP server against a real OpenSSH server that CI starts on the runner, covering every command in conn help as an agent and as a human at a terminal; a test fails when a command has no end-to-end test. Each target now gets its own shared ssh connection, not one per host, port and userEran: a local sshd is a real target and costs almost nothing. Its first run showed a second target riding another's login, which no mock could catchlocked
D49A target can name another target as its jump host (jump = "gw", conn add --jump gw). Each hop gets its own pinned, locked-down block in the generated config, linked by ProxyJump, and logs in with its own secret; a target behind a jump host has its key fetched through the pinned hops, with no credential sent, for the human to confirmEran asked for jump boxes. D7 promised ProxyJump but never built it; making the bastion a target keeps every rule on every hoplocked
D38Approving in the agent client is on by default, for every MCP client that supports it; --no-approve-in-client turns it offEran: a general MCP for all agents, and in-chat approval should be on; other clients fall back to conn watch or conn approvelocked
D37conn doctor checks the config and state dirs, ssh tools, keychain, secret sources, pinned keys, leases and the audit chainThe first real user lost time to a read-only sandbox and a locked keychainlocked
D36conn watch reads only local files and never contacts a targetIt can run beside any number of agents without racing themlocked
D35conn allow needs a terminal and a yes; conn deny does notGranting trust is human-only; taking it away is always safelocked
D34One hash-chained audit log feeds conn watch and conn doctorOne record for the live view and for review later; edits show uplocked
D33Approving in the agent client is opt-inSuperseded by D38superseded
D32The tool-schema size stays a test cap, not a published token budgetEran: no need to publish itlocked
D31No listing in the official MCP Registry yetEran: maybe in the futurelocked
D30The max_bytes ceiling is 16 KBEran: 8 to 16 KB; one call never floods the context, and paging covers the restlocked
D29conn run <target> <command...>: conn's options go before or right after the target; the first other word starts the remote command, flags included; -- is optionalAgents write conn run lab uname -a and kept hitting parser errorslocked
D28The config dir (targets, pinned keys, leases) is only read when commands run; job output, approvals and generated SSH configs live in a separate state dirAgent sandboxes mount config read-only; only add, approve, rm and lease change trustlocked
D27Unit tests run Conning's real remote scripts with the local sh behind a fake transport; CI gates coverage at 95%Eran: mock with unit tests as much as possible, 95%+ coveragelocked
D26In M1 you answer approvals at a terminal with conn approve a:<id>; one approval covers a whole fan-outWorks today; M2 brings the prompt to the agent clientlocked
D25Windows targets run synchronously in M1Superseded by D43superseded
D24POSIX jobs run detached on the target, in their own process group, read by byte offsetA dropped SSH link never kills a test run; signals reach the whole treelocked
D1The agent stays on the dev machine; Conning dials out to targetsNo LLM tunnel, no handoff zip, no LLM keys on VMslocked
D2One core with two front ends: an MCP server and the conn CLI, giving identical outputApprovals and jobs need a long-lived process; the CLI serves humans and scriptslocked
D3Agents with only bash may use the CLI directlyEran, Q1: works for any harness, including the side project'slocked
D4Single developer on their own machineEran, Q2: keep v0.1 smalllocked
D4A target file teams can commit and shareSuperseded by D4 (single dev)superseded
D5Four agent tools: targets, run, read, transferEran, Q4: tool schemas cost tokens every turn; fewer tools beat morelocked
D14Output cap is an argument: max_bytes=8192 by default, raised by the agent when needed, up to a ceiling (D30)Eran: lean by default, more on demandlocked
D15Progress streams to the human (MCP progress, conn watch, a live terminal); the agent gets new lines on each readEran wants a sense of progress; models only see a call when it returnslocked
D16Async through wait: 0 returns a job id at once, the default is 30 s, longer runs become jobsEran: optional async calls without adding a toollocked
D17Plain-text output by default, --json on requestEran, Q5: about 40% fewer tokenslocked
D18"Always allow" remembers the first two words (flags skipped), per target; shell wrappers always askEran, Q7: the danger lives in the verb (systemctl stop, git push)locked
D19v0.1 is SSH-only; a Windows host with no SSH server gets a fix the agent relays to youEran, Q8: some old images lack OpenSSH; the agent asks the user to fix itlocked
D20Password auth is first-class, stored in the keychain, delivered through askpassEran, Q9: lab VMs are usually root plus a passwordlocked
D6No homemade vault; secrets are references to the keychain, op:// or ssh-agentLess of our own crypto means fewer CVEsto confirm
D7Shell out to OpenSSH with a generated config, pinned host keys, forwarding offKeeps ProxyJump and agents working, without inheriting unsafe optionsto confirm
D8Target modes: observe, ask, lease (15 min default); new lab targets start in askEran, Q6: works at once, approve the unusuallocked
D9The agent proposes targets; they stay pending until you confirmEran wants the agent to enrol targets; one keystroke blocks injected hostslocked
D9Only humans can add targetsSuperseded by D9 (agent proposes, human confirms)superseded
D10CLI help is cached in the README the agent readsEran: saves a --help round trip every sessionlocked
D11Project is Conning, CLI conn; remote machines are "targets"Free on PyPI and npm; agents already know "target"locked
D11Project named PatchbayTaken on PyPI and npm; superseded by Conningsuperseded
D12No {{secret:x}} placeholders in agent commands in v0.1A placeholder in a free command can be sent anywhere; human-declared env injection covers the needto confirm
D21Python, installed as a tool with uv tool install conningEran, Q10: the side project's stack; official MCP SDK; few dependencieslocked
D22Public repo on GitHub, EranYonai/Conning, Apache-2.0Eran, Q11; Apache for its patent grantlocked
D23Ship as the conning package on PyPI, released by GitLab CI through PyPI Trusted Publishing on tag; run with uvx conning mcpNo stored PyPI tokens; one-line MCP setup in any clientto confirm
D13Build our own; adopt ideas from prior art, not dependenciesEran: learn from open source, keep our own server small and readablelocked

Open questions

  • When does WinRM become a second connector for old Windows images?

Posts

Newest first.

#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
CONN

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.