fix: post-0.17.2 field fixes — Host prefix case, ProxyCommand flags, 1Password agent, console output - #43
Merged
Conversation
ssh matches Host patterns case-sensitively. Blocks are recognised as ours
case-insensitively, so a config carrying a stale `Host cloudx-*` from an
older release was adopted as the generic block and written back unchanged,
above host entries spelled `cloudX-`. The generic block then matched nothing:
line 15: Applying options for cloudX-DTA-*
line 19: Applying options for cloudX-DTA-unified
(no "Applying options for cloudX-*")
so `IdentitiesOnly yes` and `User ec2-user` silently stopped applying. ssh
then offered every key in the agent, the server hit MaxAuthTries, and the
connection failed with "Too many authentication failures" before reaching the
key EC2 Instance Connect had just pushed - on an otherwise healthy session
that had already completed KEX and verified the host key.
This is a regression from 0.17.2. Compared against v0.17.1 on the same input,
the old code wrote `Host cloudX-*` and the new code wrote `Host cloudx-*`. It
came from two changes meeting: matching the generic block case-insensitively,
and keeping the first of any duplicate generic blocks. Together, a stale
lowercase block was adopted and then evicted the correct-case one that
`_check_and_create_generic_config` had just appended.
A managed block's Host line is now written with the prefix in the case
currently in use, so a file this tool writes never disagrees with itself.
Inline comments survive, only the prefix is rewritten, and a host whose own
name repeats the prefix spelling is left alone.
Existing broken configs are repaired by `cloudX-proxy cleanup`, which already
normalised the prefix across the whole file; this makes `setup` do the same
rather than leaving the file half-converted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
The ProxyCommand rebuild exists to drop flags that merely restate an auto-detected default, but it recomputes those defaults from the running process rather than from the line it is rewriting. Left to itself it deletes settings that are doing real work. Reported from the field: a config carrying `--profile cloudx` came out of cleanup with no --profile at all, because the default detected here is `cloudX`. AWS profile names are case-sensitive, so the next connection failed with "The config profile (cloudX) could not be found". `--region` was worse - the rebuild has no notion of it, so it was dropped outright. A flag the rebuild does not reproduce is now carried over rather than discarded. Cleanup may tidy a configuration; it may not decide that part of it was unnecessary. This does not change how a ProxyCommand is generated for a new environment - flags matching a detected default are still omitted there - only that rewriting an existing line cannot lose what it already said. This behaviour predates the previous release: v0.17.1 strips the flag identically. It is longstanding data loss in the same class as the rest of this file's recent fixes, not a regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
~/.1password/agent.sock was treated as the one place the agent lives. It is a Unix socket path, so on Windows the check never passed and --1password silently fell back to a plain on-disk key; had it passed, the config would have carried an IdentityAgent pointing at nothing and broken an agent that already worked. Windows needs no directive at all: 1Password serves the standard OpenSSH named pipe \\.\pipe\openssh-ssh-agent, which ssh uses by default. Check the pipe there, keep the socket (and the macOS symlink handling) elsewhere, add the Linux snap location, and write IdentityAgent only where one has to be named. Output on macOS and Linux is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The fixes are directly covered by targeted new tests, align with SSH behavior (case-sensitive Host patterns), and no correctness issues were found in the changed code paths.
Pull request overview
Fixes three post-0.17.2 SSH-config regressions/gaps affecting real connections (notably on Windows) by making managed Host patterns internally consistent, preserving user-critical ProxyCommand flags during cleanup, and selecting the 1Password SSH agent appropriately per platform.
Changes:
- Normalize managed
Host <prefix>-...lines to the configured prefix case to avoid case-sensitive pattern mismatches in OpenSSH. - Update
cleanupProxyCommand rewriting to preserve existing flags (e.g.,--profile,--ssh-key,--region,--aws-env) instead of silently dropping them. - Make 1Password SSH agent detection/config platform-aware (Windows named pipe default, macOS symlink behavior, Linux snap socket path) and document the platform differences.
File summaries
| File | Description |
|---|---|
| cloudx_proxy/setup.py | Implements Host-line prefix-case normalization, ProxyCommand flag preservation during cleanup, and per-platform 1Password agent detection/writing. |
| tests/test_ssh_config.py | Adds regression tests for prefix-case normalization and ProxyCommand flag preservation invariants. |
| tests/test_runtime_safety.py | Adds platform-matrix tests for 1Password agent detection and Windows “no IdentityAgent written” behavior. |
| README.md | Updates troubleshooting docs with correct 1Password agent location/behavior per platform. |
| .ai/context/ssh-config.md | Documents IdentityAgent/IdentityFile behavior with 1Password and platform-specific agent locations. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
ssh matches Host patterns case-sensitively, and its pattern syntax has only * and ? - `cloud[xX]-*` is a literal hostname, not a character class. So a block written as `Host cloudX-*` does not apply to a host entry spelled `cloudx-dev-web1`, and both spellings are out there: two command names, older releases, and hand edits all put them in the same file. When they disagree the generic block stops applying, which is how `IdentitiesOnly yes` and `User ec2-user` go missing and ssh runs into MaxAuthTries. Normalising the case (the previous fix) keeps a file we wrote consistent, but only refuses to add to a mixed one. A Host line takes any number of patterns and matches if any of them does, so wildcard blocks now list one pattern per spelling: `Host cloudX-* cloudx-*`. Host entries keep a single name in the configured case - they are what `list` reports and what VSCode offers. The parser has to recognise its own output: a multi-pattern Host line was classed as the user's, so without this a rewrite would file our own block under "not managed" and generate a duplicate. Patterns that differ by more than case still name different hosts and stay the user's. Verified against OpenSSH 9.6 with `ssh -G`: both spellings of a host entry resolve User, IdentitiesOnly, IdentityFile and ProxyCommand from the widened blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
Only the X in the prefix is ambiguous. cloudX is the product's name - the X is ten, after Cloud9 - but people who would rather not reach for shift call their instance cloudx-dev-something, and that name is theirs: it is what they type, what list reports and what VSCode offers. The environment part is fixed by whoever rolled out the environment stack, and the host part is the user's. Widening the wildcard blocks to both spellings removed the reason to rewrite anything else, so stop: - _normalize_managed_host_line leaves entries alone and only widens patterns - cleanup's prefix conversion applies to wildcard patterns and the ProxyCommand, not to entry names, so switching command name no longer renames someone's box - re-running setup for an existing host updates its HostName under the name it already has; only a brand new entry uses the configured case Verified with ssh -G: an entry left as cloudx-DTA-lower keeps its name, takes its new instance id, and still resolves User, IdentitiesOnly and ProxyCommand from the cloudX-DTA-* cloudx-DTA-* block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
A pattern has no owner; a host does. Host entries are listed under the names their owners gave them, case and all, but a pattern was being rendered in whichever case the command name implied - so cloudx-proxy list showed cloudx-dev-* for a config written by cloudX-proxy, and the other way round. Show patterns as cloudX-*: the X is ten, after Cloud9, and it is the spelling to put in front of a user. An unrelated prefix is still shown as configured. An environment with no hosts of its own already appeared only among the patterns, since the listing below them is built from host entries; that is now covered by a test rather than left to chance. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
print_header prepended two newlines while the banner above it appended one, so the first section sat under three blank lines and every section after it under two. One blank line separates sections now. Banners say cloudX-proxy whichever command name was typed, matching what list does with patterns: cloudX is the product's name. The setup summary's "Connect using: ssh ..." was built from the configured prefix, so after the previous commit it could name a host that does not exist - an entry already in the config keeps its own name. It reports the entry that was actually written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
Indents had drifted: an indent of 3 in the migration preview, details at 2 with no step above them (Prerequisites, cleanup, migration, the legacy-config check), and the instance check reporting at 4 with nothing at 2 to belong to. Symbols had drifted the same way - a condition the code went on to recover from was marked ✗, so a snap 1Password install saw a cross immediately followed by a tick, and one genuine failure produced four marked lines. The grid is now 0 for a step, 2 for a detail of it, 4 for a detail of a nested operation, and nothing else; ○ for neutral, ✓ for true now, ✗ for the outcome when it is wrong. The rule is written next to print_status, where it is enforced, and in .ai/context/development.md. Also brings the raw prints that sat off the grid onto it (the credentials prompt, the 1Password vault menu), and fixes the Windows call-out box naming a host from the configured prefix rather than the entry that was written. Tests walk a full dry-run and a cleanup, asserting every status line sits on the grid, that no detail is orphaned, and that one failure shows one ✗. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
"Would reorganize 1 environments" reads like a bug in the tool. A plural()
helper renders a count with the noun that agrees with it, taking an explicit
plural where adding 's' is wrong ("1 host entry" / "2 host entries").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
The host prefix is taken from the command that was invoked, by comparing basename(sys.argv[0]) with 'cloudX-proxy'. On Windows a console script is an .exe, so that basename is 'cloudX-proxy.exe' and never matched: every Windows user silently got the lowercase prefix whichever of the two commands they typed. Compare the stem instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
The preview showed 'cloudX-*' while the write produces 'cloudX-* cloudx-*', so the one command meant for looking before you leap did not show the change this release makes. Route the preview through the same helper as the write. Host entries stay a single name, as they are written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Eleven commits. Nine fixes for problems found after 0.17.2 shipped — two of them regressions from that release that broke real connections on Windows, the rest closing the gaps those regressions exposed — plus two cosmetic passes over the console output.
Blast radius note:
connect— the only code pathuvxpulls into every user's SSH session — is not in this diff.core.pyis untouched andcli.py'sconnectcommand is unchanged. Everything here runs only when someone deliberately runssetup,cleanup,listormigrate, andcleanupwrites a.bakfirst.Behaviour fixes
1.
fix: write managed Host lines in one prefix case(b551fe1)Symptom:
Too many authentication failureson a host that worked before upgrading.The 0.17.2 parser matches
Hostkeywords case-insensitively (correct — ssh keywords are), but that made it adopt a stale lowercaseHost cloudx-*block found in an existing config and use it in place of the correctly-cased one. ssh's pattern matching is case-sensitive, soHost cloudx-*never applied tocloudX-DTA-unified: the generic block'sIdentitiesOnly yesandUser ec2-usersilently stopped applying, ssh offered every key in the agent, and the server hitMaxAuthTriesfirst.2.
fix: cleanup no longer deletes ProxyCommand flags(5958bba)Symptom:
The config profile (cloudX) could not be foundafter runningcleanup.cleanuprebuilds eachProxyCommandfrom current state, which drops any flag the rebuild doesn't reproduce —--profile,--ssh-keyand--regionwere being deleted from working hosts. This one predates the branch (reproduced against v0.17.1), but it is reachable from the same upgrade path._rebuild_proxy_command()now parses the existing flags first and re-appends anything the rebuild didn't produce, so a rewrite can only add, never silently drop.3.
fix: find the 1Password SSH agent per platform(8ae4860)~/.1password/agent.sockwas treated as the one place the agent lives. That is a Unix socket path, so on Windows the check could never pass and--1passwordsilently fell back to a plain on-disk key — and had it passed, the config would have carried anIdentityAgentpointing at nothing, breaking an agent that already worked.\\.\pipe\openssh-ssh-agent, which ssh uses by default; the check probes the pipe namespace instead of looking for a file.~/snap/1password/current/.1password/agent.sockfor snap installs, used where it lives rather than symlinked into a home the snap cannot see.4.
fix: wildcard Host blocks match every spelling of the prefix(4532ecf)Fix 1 keeps a file we wrote self-consistent, but it only refuses to add to a config that is already mixed — and a config legitimately becomes mixed when someone runs
cloudX-proxy setuponce andcloudx-proxy setuplater. ssh's pattern syntax offers no way to cover both in one pattern: matching is case-sensitive and only*and?are supported, socloud[xX]-*is a literal hostname, not a character class.A
Hostline does take any number of patterns and matches if any one of them does, so wildcard blocks now always list one pattern per spelling:Only the prefix varies. The environment part is fixed by whoever rolled out the environment stack and the host part is the user's; both are written exactly as given.
The parser had to be taught to recognise its own output: a multi-pattern
Hostline was classified as the user's, so without that change a rewrite would file our own block under "not managed" and generate a duplicate alongside it. Patterns that differ by more than case still name genuinely different hosts and stay the user's.5.
fix: never rename a host entry to the command name's case(1b19d32)cloudXis the product's name — the X is ten, after Cloud9 — but people who would rather not reach for shift call their instancecloudx-dev-something, and that name is theirs: it is what they type, whatlistreports and what VSCode offers. Widening the wildcard blocks removed the reason to rewrite anything else, so:_normalize_managed_host_line()leaves entries alone and only widens patternscleanup's prefix conversion applies to wildcard patterns and theProxyCommand, not to entry names — switching command name no longer renames someone's boxsetupfor an existing host updates itsHostNameunder the name it already has; only a brand new entry uses the configured case6.
fix: list shows patterns in the preferred spelling(0c49ec5)A pattern has no owner; a host does. Hosts are listed under the names their owners gave them, but a pattern was rendered in whichever case the command name implied — so
cloudx-proxy listshowedcloudx-dev-*for a config written bycloudX-proxy. Patterns are now shown ascloudX-*either way; an unrelated prefix is still shown as configured. An environment with no hosts of its own appears only among the patterns (under--detailed), never as an environment heading.7.
fix: stop stacking blank lines, and name the host that was written(3e24b87)print_header()prepended two newlines while the banner above it appended one, so the first section ofsetupsat under three blank lines and every section after it under two. One blank line separates sections now, and banners saycloudX-proxywhichever command name was typed.Also fixes a consequence of #5: the setup summary's
Connect using: ssh …was built from the configured prefix, so it could name a host that does not exist once an existing entry keeps its own name. It reports the entry actually written.8.
fix: detect the command name on Windows too(3f537c4)The host prefix is taken from the command that was invoked, by comparing
basename(sys.argv[0])with'cloudX-proxy'. On Windows a console script is an.exe, so that basename iscloudX-proxy.exeand never matched: every Windows user has silently been getting the lowercase prefix whichever of the two commands they typed, on every released version. Compares the stem instead.This is very likely the origin of the mixed-case configs #1 and #4 exist to survive: a Mac writes
cloudX-*blocks, a Windows box writescloudx-*ones into the same file.Confirmed on a real Windows machine:
setup --dry-runnow previewscloudX-*where it previously previewedcloudx-*.9.
fix: dry run previews the patterns it will actually write(d0911ac)Found by that same Windows run. The preview showed
cloudX-*while the write producescloudX-* cloudx-*— the one command meant for looking before you leap did not show the change this release makes. The preview goes through the same helper as the write. Host entries stay a single name, as they are written.Console output
10.
style: put console output on one grid(a103b36)Three indents and three symbols, used the same way everywhere, so a run can be skimmed down the left edge. The rule lives next to
print_status()where it is enforced, and in.ai/context/development.md.○neutral (about to happen, in progress, informational, dry-run preview) ·✓true now ·✗wrong.Indents had drifted: an indent of 3 in the migration preview, details at 2 with no step above them (
Prerequisites,cleanup, migration, the legacy-config check), and the instance check reporting at 4 with nothing at 2 to belong to.cleanuphad no section header at all. Four rawprint()calls sat off the grid entirely.Symbols had drifted the same way. The rule that was missing: a ✗ marks the outcome, not every observation on the way to it. The 1Password check reported "socket not found at
~/.1password/agent.sock" as a failure before it had looked anywhere else, so a snap install saw a cross immediately followed by a tick, and one genuine failure produced four marked lines. Same shape for a missing AWS profile that is then created. Both neutral now.Also fixes the Windows call-out box (
Setup completed! To test your SSH connection, run: ssh …), which built its hostname from the configured prefix — the same bug as #7's summary line.11.
style: make counts agree with their noun(27b98dc)Would reorganize 1 environmentsreads like a bug in the tool. Aplural()helper takes an explicit plural where addingsis wrong (1 host entry/3 host entries).Testing
ruff checkclean.ssh -G, not by reasoning:Hostpatterns are case-sensitive;[xX]is matched literally; multiple patterns on oneHostline OR correctly; and an entry left ascloudx-DTA-lowerkeeps its name, takes a new instance id, and still resolvesUser,IdentitiesOnlyandProxyCommandfrom thecloudX-DTA-* cloudx-DTA-*block.liststill finds the hosts, andssh -GresolvesHostName,IdentityFileandProxyCommandidentically. Running the oldcleanupon such a config is cosmetically messy — the widened blocks land underNOT MANAGEDand an empty duplicate env block is generated above them — but the result still resolves correctly, because the empty block supplies no directives so the preserved one wins.tests/test_backward_compat.pykeeps its v0.17.1 fixtures frozen. The two "cleanup changes nothing" tests now allow exactly two differences — the version stamp, and a wildcardHostline gaining the other spelling — and still pin the line count, so nothing may be added or dropped and no host entry may change.TestOpAgentPerPlatform(7),TestBothPrefixSpellingsMatch(13),TestListShowsThePreferredSpelling(5),TestOutputGrid(5),TestOneFailureOneCross(2),TestCountsAgreeWithTheirNoun+TestPluralHelper(6),TestPrefixFromCommandName(2, covering bothntpathandposixpathargv shapes),TestDryRunPreviewsWhatIsWritten(3), plus rewrittenTestNormalizeManagedHostLinecases.Upgrade note
The first
setuporcleanupa user runs after this release rewrites their wildcardHostlines to the two-pattern form. That is the fix in #4, the backward-compat tests pin it to exactly that change plus the version stamp, and nothing else in the file moves — but it is the moment their config visibly changes, so it is worth a line in the release notes.Docs
README documents the widened patterns, what
cleanupnow converts, that instance names are never rewritten, and howlistrenders the two..ai/context/ssh-config.mdgains a "Prefix Case" section explaining the ssh pattern-matching constraint and which parts of a host name belong to whom, plus theIdentityAgentrule and whyIdentityFilenames the public key under 1Password..ai/context/development.mdgains a "Console Output" section with the grid.🤖 Generated with Claude Code
https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd