Repository navigation
feat: scan and restore the SSH client config (plan 16 Task 2) - #109
Merged
Merged
Conversation
15 of 18 tasks
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.
Summary
Plan 16 Task 2 (
docs/plans/2026-11-05-16-ssh-config.md): a newsshprovider scans~/.ssh/configand the files itIncludes from inside~/.ssh, and the export restores them with OS-specific lines commented out, private permissions on macOS/Linux, and a "Copy these SSH keys yourself" list. Keys are never read or exported. Spec §2/§6/§8/§10/§15 sections touched by plan 16 were already merged with the plan. This PR adds the details the implementation settled (see Deviations).Type
Changes
Category::Ssh("ssh"),FindingKind::KeyFile { path },FindingKind::ExternalInclude { path },TargetPath::is_private()(true when aHometarget's first part is.ssh).ssh(providers/ssh.rs, registered aftershell): reads~/.ssh/configand followsIncludeonly inside~/.ssh(~/.ssh/…, relative, or absolute under home/.ssh). It does not follow.., paths outside~/.ssh, wildcards before the last part, or%/$tokens. Each of those lines gets anExternalIncludefinding. Wildcards match*/?in the last part only, in name order, and skip dot files like glob(3). Limits: depth 16, 32 files, one item per file. Forbidden names (known_hosts*,authorized_keys*,id_*,*.pub,environment,rc, compared case-insensitively) and files holding a private key block never become items.IdentityFiletargets are never opened or checked. Findings: secrets, then by lineExternalInclude/OsSpecificLine(UseKeychain, absolute or~/Library/paths) /KeyFile.ssh::is_config_targetis shared by the scan andrepot.jsonopening (snapshot::is_scanned_config_targetgets anssharm). Other providers' allowlists already reject.ssh/, so ansshitem can only write SSH config files under.ssh/.file:ssh-config+catalogkeys in all four desktop locales (en/ko/ja/zh-Hans).SetEnv NAME=value/SetEnv=NAME=value(any case) is masked through the name rule, to the end of the line.prepare_config: when the OS kind differs, it comments outUseKeychain(destination not macOS),ControlMaster/ControlPath/ControlPersist(destination Windows), andIdentityFile/CertificateFile/IdentityAgent/UserKnownHostsFile/ControlPathwith an absolute or~/Library/value. Line numbers stay the same, and the comment format is shared with git throughcomment_out. It always emitsExportNote::SshKeyforIdentityFile/CertificateFilevalues, including lines it commented out.copy_private_file(umask 077 subshell +chmod 600) is emitted only when a target is under~/.ssh. New dirs are 0700 and files 0600. An existing~/.sshkeeps its mode, and the backup keeps the old file's mode (cp -p).Copy these SSH keys yourself (Repot does not export keys):lists each path once (first line wins). Without key lines it names the default keys. SSH keys are kept out ofNotes. The manifest README adds achmod 700/chmod 600line for macOS/Linux. The script-format README is now also written when SSH keys are listed.scanlists anSSHcategory afterShell.crates/cli/README*.mddo not list categories, so they are unchanged.render_samples: has.ssh/configand.ssh/config.d/workcopies and anSshKeynote, so CI's shellcheck and PSScriptAnalyzer lint the new code.ssh_test.rs(13 tests,FakeEnvplus a recordingEnvthat lists dirs in reverse and logs reads),SetEnvtable, payload destination table (macOS→macOS/Linux/Windows, Linux→macOS), bash run test (modes, backup mode, idempotence, summary text), PowerShell and manifest key lists,repot.json.ssh/rules + SSH round trip (direct == via file for 3 targets), compare (same / differs / Linux file that differs only by the commentedUseKeychainis Present), CLI category order.ExternalIncludeto Global Constraints, the example JSON and the model section.includesreturns line numbers. The plan also records the finding order and how the snapshot rule is implemented, and Task 2 is ticked. The spec addsExternalIncludeto the model andsshto the §10 allowlist sentence.Checks
cargo fmt --checkcargo clippy --workspace --all-targets -- -D warningscargo test --workspacepnpm --filter @repot/desktop typecheck/test/build--example scan/--example export, ortauri dev)restore.ps1(no local pwsh), PowerShell runtime behaviour, and the Windows/Linux runners.Also ran:
pnpm exec biome ci .,cargo test -p repot-core --example e2e_export --example smoke_export --example cross_os_e2e --example render_samples,shellcheck --severity=warningon therender_samplesbash output (clean). Mutation pass: each rule inssh.rs,prepare_config,copy_private_file, the key list, the README condition, the snapshot arm andSetEnvwas broken once. 34 of 34 mutations fail a test. Three early survivors got tests (#comments inInclude, Linux→macOS keepsUseKeychain, UNC paths). A fourth was an extra ".ssh/only forssh" guard that no input could reach, because every other provider's allowlist already rejects.ssh/. It was removed, and the plan now says so.Real data (throwaway
examples/zz_ssh.rs, deleted, scanning read-only):ssh:file:.ssh/configwithExternalInclude ~/.orbstack/ssh/config(line 4). Only~/.ssh/configwas read, and nothing under.orbstack. The macOS/Linux/Windows exports read back with 0 skipped items. The macOSrestore.shin a tempHOME(XDG pinned) gave~/.ssh700 andconfig600. The restored file is identical to the source, and the second run reportsalready done. With an existing 755~/.sshand a 644 config, the dir stays 755, the config becomes 600 and the backup stays 644.ssh -G -F $tmp/.ssh/config github.comexits 0. The real file has no host alias, sogithub-workwas checked as below.HOME: the findings, the Linux/Windows payload diffs and the summary match the plan's 예시 exactly. After restoring,ssh -G -F $tmp/.ssh/config github-workexits 0 with hostname/user/both identity files, and modes are 700/700/600/600.restore.shreports SC2034 (PREFLIGHT_MISSINGunused). This is pre-existing: the helper is always written but only used when a step needs a tool, and this PR does not change it. CI lintsrender_samples, which has tools, so CI is clean.Spec and generated files
scan.json,snapshots/,icons/)scan.jsonis unchanged:ui_fixture.rsgets its SSH item in Task 3, and the desktop suite passes on the current fixture. The four snapshot diffs each add only the.ssh/configcopy,copy_private_file, the SSH key list and the README chmod line.Boundaries
repot-coredoes not depend on Tauri and does no network callsEnv; tests useFakeEnvand fixtures, not real tools