Skip to content

fix: post-0.17.2 field fixes — Host prefix case, ProxyCommand flags, 1Password agent, console output - #43

Merged
ErikMeinders merged 11 commits into
mainfrom
claude/code-review-improvements-rv1ipu
Sep 6, 2026
Merged

ErikMeinders merged 11 commits into
mainfrom
claude/code-review-improvements-rv1ipu

Conversation

@ErikMeinders

@ErikMeinders ErikMeinders commented Sep 5, 2026 •

Copy link
Copy Markdown
Member

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 path uvx pulls into every user's SSH session — is not in this diff. core.py is untouched and cli.py's connect command is unchanged. Everything here runs only when someone deliberately runs setup, cleanup, list or migrate, and cleanup writes a .bak first.

Behaviour fixes

1. fix: write managed Host lines in one prefix case (b551fe1)

Symptom: Too many authentication failures on a host that worked before upgrading.

The 0.17.2 parser matches Host keywords case-insensitively (correct — ssh keywords are), but that made it adopt a stale lowercase Host cloudx-* block found in an existing config and use it in place of the correctly-cased one. ssh's pattern matching is case-sensitive, so Host cloudx-* never applied to cloudX-DTA-unified: the generic block's IdentitiesOnly yes and User ec2-user silently stopped applying, ssh offered every key in the agent, and the server hit MaxAuthTries first.

2. fix: cleanup no longer deletes ProxyCommand flags (5958bba)

Symptom: The config profile (cloudX) could not be found after running cleanup.

cleanup rebuilds each ProxyCommand from current state, which drops any flag the rebuild doesn't reproduce — --profile, --ssh-key and --region were 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.sock was treated as the one place the agent lives. That is a Unix socket path, so on Windows the check could never pass and --1password silently fell back to a plain on-disk key — and had it passed, the config would have carried an IdentityAgent pointing at nothing, breaking an agent that already worked.

  • Windows — no directive is written at all. 1Password serves the standard OpenSSH named pipe \\.\pipe\openssh-ssh-agent, which ssh uses by default; the check probes the pipe namespace instead of looking for a file.
  • macOS — unchanged, symlink create/retarget and the "don't delete a live socket" prompt included.
  • Linux — the default socket, plus ~/snap/1password/current/.1password/agent.sock for 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 setup once and cloudx-proxy setup later. ssh's pattern syntax offers no way to cover both in one pattern: matching is case-sensitive and only * and ? are supported, so cloud[xX]-* is a literal hostname, not a character class.

A Host line does take any number of patterns and matches if any one of them does, so wildcard blocks now always list one pattern per spelling:

Host cloudX-* cloudx-*
Host cloudX-dev-* cloudx-dev-*

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 Host line 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)

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. Widening the wildcard blocks removed the reason to rewrite anything else, so:

  • _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 — 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

6. 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 list showed cloudx-dev-* for a config written by cloudX-proxy. Patterns are now shown as cloudX-* 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 of setup sat under three blank lines and every section after it under two. One blank line separates sections now, and banners say cloudX-proxy whichever 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 is cloudX-proxy.exe and 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 writes cloudx-* ones into the same file.

Confirmed on a real Windows machine: setup --dry-run now previews cloudX-* where it previously previewed cloudx-*.

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 produces cloudX-* 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.

=== Section ===          a phase of the command
○ Doing something...     STEP:   one operation in the section     (indent 0)
  ✓ It worked            DETAIL: what that step found or did      (indent 2)
    ○ ...                SUB:    detail of a nested operation     (indent 4)

○ 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. cleanup had no section header at all. Four raw print() 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 environments reads like a bug in the tool. A plural() helper takes an explicit plural where adding s is wrong (1 host entry / 3 host entries).

Testing

  • 253 tests pass; ruff check clean.
  • Verified against real OpenSSH 9.6 with ssh -G, not by reasoning: Host patterns are case-sensitive; [xX] is matched literally; multiple patterns on one Host line OR correctly; and an entry left as cloudx-DTA-lower keeps its name, takes a new instance id, and still resolves User, IdentitiesOnly and ProxyCommand from the cloudX-DTA-* cloudx-DTA-* block.
  • Verified on a real Windows machine (feat: Add --dry-run flag for preview mode #8, chore(deps): bump actions/checkout from 4 to 5 #9).
  • Downgrade checked, by running the current release's code from a worktree against a config shaped the way this PR leaves it: old list still finds the hosts, and ssh -G resolves HostName, IdentityFile and ProxyCommand identically. Running the old cleanup on such a config is cosmetically messy — the widened blocks land under NOT MANAGED and 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.py keeps its v0.17.1 fixtures frozen. The two "cleanup changes nothing" tests now allow exactly two differences — the version stamp, and a wildcard Host line gaining the other spelling — and still pin the line count, so nothing may be added or dropped and no host entry may change.
  • New coverage: TestOpAgentPerPlatform (7), TestBothPrefixSpellingsMatch (13), TestListShowsThePreferredSpelling (5), TestOutputGrid (5), TestOneFailureOneCross (2), TestCountsAgreeWithTheirNoun + TestPluralHelper (6), TestPrefixFromCommandName (2, covering both ntpath and posixpath argv shapes), TestDryRunPreviewsWhatIsWritten (3), plus rewritten TestNormalizeManagedHostLine cases.

Upgrade note

The first setup or cleanup a user runs after this release rewrites their wildcard Host lines 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 cleanup now converts, that instance names are never rewritten, and how list renders the two. .ai/context/ssh-config.md gains a "Prefix Case" section explaining the ssh pattern-matching constraint and which parts of a host name belong to whom, plus the IdentityAgent rule and why IdentityFile names the public key under 1Password. .ai/context/development.md gains a "Console Output" section with the grid.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NiMd9TR2SJ4PAY4tmSBWzd

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
Copilot AI lite review requested due to automatic review settings September 5, 2026 07:42

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 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 cleanup ProxyCommand 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
@ErikMeinders ErikMeinders changed the title fix: post-0.17.2 field fixes — Host prefix case, ProxyCommand flags, 1Password agent per platform fix: post-0.17.2 field fixes — Host prefix case, ProxyCommand flags, 1Password agent, console output Sep 5, 2026
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
@ErikMeinders
ErikMeinders merged commit 6e08021 into main Sep 6, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants