Skip to content

SSH config parser does not strip quotes from IdentityFile values #7

Description

@andrewfung729

Shuttle's SSH config parser (src/shuttle/core/ssh_config.py) leaves surrounding quotes on IdentityFile values. Because the quotes are not removed, SSHConfigEntry.resolve_key() tries to resolve a literal path that still contains the quote characters, and fails to find the key file.

Reproduction

With the following ~/.ssh/config:

Host myserver
    HostName 10.0.0.1
    IdentityFile "~/.ssh/id_rsa"

Run:

from pathlib import Path
from shuttle.core.ssh_config import parse_ssh_config

# Make sure the key file exists
key = Path.home() / ".ssh" / "id_rsa"
key.write_text("KEY")

entries = parse_ssh_config()
print(entries[0].identity_file)  # '"~/.ssh/id_rsa"'  (includes double quotes)
print(entries[0].resolve_key())   # None

Expected behavior

entry.identity_file should be ~/.ssh/id_rsa (without the surrounding double quotes), and entry.resolve_key() should return the actual key path.

Actual behavior

entry.identity_file is "~/.ssh/id_rsa" (the quotes are part of the string), and resolve_key() returns None.

Additional notes

  • The same problem occurs with single quotes: IdentityFile '~/.ssh/id_rsa'.
  • Paths containing spaces, e.g. IdentityFile "~/.ssh/my key", are also mishandled. The current parser uses str.split(None, 1), which splits at the first whitespace inside the quoted value and drops the rest of the path.

Environment

  • macOS
  • Python 3.12+
  • Shuttle main branch

Proposed solution

Replace the ad-hoc split(None, 1) / split("=", 1) logic with a regex that captures the whole rest of the line, so quoted values containing spaces are preserved. Then strip a single pair of matching outer quotes (" or ') from every value.

In src/shuttle/core/ssh_config.py:

  1. Add a line regex that treats whitespace or = as the key/value separator, allowing optional spaces around =:

    _LINE_RE = re.compile(r"^(\S+?)(?:\s*=\s*|\s+)(.*)$")
  2. Add a small _unquote helper:

    def _unquote(value: str) -> str:
        value = value.strip()
        if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
            return value[1:-1]
        return value
  3. In the parse loop, match the line with _LINE_RE, then unquote the captured value and skip empty values:

    match = _LINE_RE.match(line)
    if not match:
        continue
    
    key, value = match.group(1).strip(), _unquote(match.group(2))
    if not value:
        continue

This handles:

  • IdentityFile "~/.ssh/id_rsa" → ~/.ssh/id_rsa
  • IdentityFile "~/.ssh/my key" → ~/.ssh/my key
  • IdentityFile = "~/.ssh/id_rsa" → ~/.ssh/id_rsa

Tests to add

In tests/test_core/test_ssh_config.py, add cases that:

  1. Parse IdentityFile "~/.ssh/quoted_key" and verify the quotes are stripped and resolve_key() finds the file.
  2. Parse IdentityFile "~/.ssh/my key" (with spaces inside the quotes) and verify the full path is kept and resolve_key() finds the file.

Offer to contribute

If the maintainers accept this approach, I’m happy to open a PR with the proposed fix and the corresponding tests.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions