Skip to content

feat: add clihost rsync sync command - #96

Merged
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-13/feat/rsync-sync-command-92
Jul 1, 2026
Merged

feat: add clihost rsync sync command#96
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-13/feat/rsync-sync-command-92

Conversation

@axisrow

@axisrow axisrow commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Closes #92

Summary

  • Add bin/clihost-sync.sh, a host-side rsync-over-SSH helper with pull as host -> container and push as container -> host.
  • Restrict sync to the safe subset: ~/.gitconfig and ~/.config/gh only.
  • Install the helper into the image at /bin/clihost-sync.sh and document usage in README, CLAUDE.md, and .env.example.

Safety checks

  • Dry-run is the default; real transfers require --apply.
  • Apply mode adds rsync --backup --backup-dir=....
  • --delete is never passed unless --allow-delete is explicit.
  • Remote preflight aborts unless the home volume is mounted exactly at /home/hapi.
  • Symlink guards reject controlled /home/hapi paths and existing symlinks under the synced gh tree; rsync also uses --no-links.
  • Non-goals remain out of scope: no ~/.claude, private ~/.ssh, or Claude settings.json sync.

Validation

  • python -m pytest tests/ -> 387 passed
  • docker build -t clihost:issue-92-sync .
  • docker run --rm --entrypoint bash clihost:issue-92-sync -lc 'test -x /bin/clihost-sync.sh && /bin/clihost-sync.sh --help >/tmp/clihost-sync-help 2>&1 && grep -q "clihost-sync.sh pull" /tmp/clihost-sync-help'

axisrow and others added 2 commits July 1, 2026 20:32
…d RCE (#92)

Cycle-review (PR #96) — both Codex and the Claude reviewer independently
reproduced host RCE: printf %q guards SHELL injection but NOT OPTION injection.
A target starting with '-' is parsed by ssh/rsync as a flag, so
CLIHOST_SSH_TARGET=-oProxyCommand=<cmd> runs <cmd> LOCALLY on the host during the
preflight ssh — before any mount/symlink guard can contain it. Same class as the
dashboard ProxyCommand issue (#85). Two reachable sinks: the ssh preflight and
the rsync source/dest operands.

Fix:
- reject a target matching `-*` up front (die before any ssh/rsync).
- defence-in-depth: `--` end-of-options before the ssh host and before the rsync
  positional operands, so a `-`-leading value can never be parsed as a flag.

Tests: test_option_like_target_is_rejected_before_ssh_or_rsync plants
`-oProxyCommand=touch <canary>` and asserts the run aborts and the canary never
appears (fails on old code — canary was created); test_ssh_and_rsync_calls_use_
end_of_options_separator pins the `--` separators. Verified behaviorally: the
hostile target is rejected, no ProxyCommand runs.

pytest tests/ -> 389 passed, 208 subtests.

Refs #92

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vecu5xo9D9Cs2vmeohKVKv
@axisrow

axisrow commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1)

Reviewed locally (Claude subagent + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
FIX codex + claude Option-injection: a --leading CLIHOST_SSH_TARGET (e.g. -oProxyCommand=<cmd>) is parsed by ssh as a flag → runs <cmd> on the host during preflight, before any guard. Both reviewers reproduced host RCE. Second sink: rsync source/dest. clihost-sync.sh:108, 323
claude CLEAN: fail-closed mount, --delete only via --allow-delete, dry-run default, symlink guards + --no-links, non-goals (no .claude/.ssh/settings.json), backups, port/identity validation — all verified & tested

FIX applied (1f0daa3): reject a target matching -* up front (die before any ssh/rsync); defence-in-depth -- end-of-options before the ssh host and rsync operands. Same class as the dashboard ProxyCommand issue (#85) — printf %q guards shell injection but not option injection.

Two regression tests: test_option_like_target_is_rejected_before_ssh_or_rsync (plants -oProxyCommand=touch <canary>, asserts abort + canary never created — fails on old code) and test_ssh_and_rsync_calls_use_end_of_options_separator. Verified behaviorally: hostile target rejected, no ProxyCommand runs. pytest tests/ → 389 passed, 208 subtests.

Note: the rest of the safety envelope was genuinely well-built and well-tested — the whole scheme hinged on this one unvalidated input.

Cycle 1/3 complete: 1 FIX (fixed, both sinks). No remaining FIX → review cycle done. Merge is yours to trigger (local mode does not auto-merge).

@axisrow
axisrow marked this pull request as ready for review July 1, 2026 15:13
@axisrow
axisrow merged commit 90e888f into main Jul 1, 2026
2 checks passed
@axisrow
axisrow deleted the ao/clihost_af28bd122d-13/feat/rsync-sync-command-92 branch July 1, 2026 15:13
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.

Sync: rsync-over-SSH команда для безопасного подмножества

1 participant