Skip to content

Instantly share code, notes, and snippets.

@cprima
Last active September 14, 2025 13:09
Show Gist options
  • Select an option

  • Save cprima/07977c84ffa7992117c22223a1918fe3 to your computer and use it in GitHub Desktop.

Select an option

Save cprima/07977c84ffa7992117c22223a1918fe3 to your computer and use it in GitHub Desktop.
Manage-WslSshBridge: PowerShell Script for SSH Agent Forwarding to WSL2

WSL SSH Agent Bridge

Securely share Windows SSH keys with WSL without compromising security.

This PowerShell script creates a secure bridge between Windows SSH agent and WSL environments, enabling seamless SSH key usage in WSL without mounting filesystems or enabling risky WSL interop features.

Why Use This?

  • Use existing Windows SSH keys in WSL - No need to copy private keys
  • Maintain security - No filesystem mounts or Windows PATH pollution in WSL
  • Works with any WSL distro - Ubuntu, NixOS, Alpine, etc.
  • Persistent setup - Configure once, works across WSL restarts
  • Network isolated - TCP connection limited to WSL-Windows bridge interface

Quick Start

Prerequisites

Windows:

  • OpenSSH client installed and SSH agent running
  • nmap package (for ncat): choco install nmap or download from nmap.org
  • npiperelay: choco install npiperelay
  • SSH keys ready (script will load them automatically)

WSL:

  • socat installed: sudo apt install socat (Ubuntu/Debian) or equivalent for the distro

Basic Usage

  1. Download the script to a Windows machine
  2. Run it (will restart the bridge by default):
    .\Manage-WslSshBridge.ps1
  3. Test in WSL:
    export SSH_AUTH_SOCK="$HOME/.ssh/agent.sock"
    ssh-add -l
    ssh -T [email protected]

Advanced Usage

Command Options

# Start the bridge with a specific WSL distro
.\Manage-WslSshBridge.ps1 -Action Start -Distro "Ubuntu-22.04"

# Stop the bridge completely
.\Manage-WslSshBridge.ps1 -Action Stop

# Use a different SSH key
.\Manage-WslSshBridge.ps1 -WinKey "$env:USERPROFILE\.ssh\id_ed25519"

# Load multiple SSH keys
.\Manage-WslSshBridge.ps1 -WinKey "$env:USERPROFILE\.ssh\id_rsa","$env:USERPROFILE\.ssh\id_ed25519"

# Alternative syntax for multiple keys
.\Manage-WslSshBridge.ps1 -WinKey "C:\keys\work.pem" -WinKey "C:\keys\personal.pem"

Make It Persistent

For the bridge to start automatically when opening WSL, set up a system service:

  1. Create the bridge script in WSL at /usr/local/bin/wsl-ssh-bridge:

    #!/bin/bash
    set -euo pipefail
    SOCK=/run/ssh-agent.sock
    HOST="$(ip route | awk '/^default via/{print $3; exit}')"
    rm -f "$SOCK" || true
    exec socat UNIX-LISTEN:"$SOCK",fork,mode=0666,unlink-early TCP:"$HOST":2626
  2. Create systemd service at /etc/systemd/system/wsl-ssh-agent-bridge.service:

    [Unit]
    Description=WSL to Windows SSH Agent Bridge
    After=network-online.target
    Wants=network-online.target
    
    [Service]
    Type=simple
    ExecStart=/usr/local/bin/wsl-ssh-bridge
    Restart=always
    RestartSec=2
    StandardOutput=journal
    StandardError=journal
    
    [Install]
    WantedBy=multi-user.target
  3. Enable and start:

    sudo chmod +x /usr/local/bin/wsl-ssh-bridge
    sudo systemctl enable wsl-ssh-agent-bridge.service
    sudo systemctl start wsl-ssh-agent-bridge.service
  4. Auto-export for all users in /etc/profile.d/ssh-agent-bridge.sh:

    [ -S /run/ssh-agent.sock ] && export SSH_AUTH_SOCK=/run/ssh-agent.sock

How It Works

The script creates a secure network tunnel between Windows and WSL:

Windows SSH Agent → Named Pipe → npiperelay → ncat (TCP:2626)
                                                      ↓
WSL Unix Socket ← socat ← TCP connection ←────────────┘
  • Windows side: ncat server listens on the WSL network interface
  • WSL side: socat client connects to create a Unix socket
  • Security: Connection isolated to WSL-Windows bridge network
  • Protocol: Standard SSH Agent Protocol (RFC 4252) over TCP

Troubleshooting

"Bridge FAILED: socat not found"

Install socat in the WSL distro:

  • Ubuntu/Debian: sudo apt install socat
  • Alpine: sudo apk add socat
  • Arch: sudo pacman -S socat

"ncat.exe not found"

Install nmap on Windows:

  • Chocolatey: choco install nmap
  • Direct download: nmap.org

"npiperelay.exe not found"

Install npiperelay:

choco install npiperelay

Or download a pre-built binary from the releases page.

SSH agent has no keys

Load SSH keys in Windows:

ssh-add "$env:USERPROFILE\.ssh\id_rsa"

Security Notes

  • The bridge uses a TCP connection limited to the WSL-Windows network interface
  • No filesystem mounts or Windows PATH access required in WSL
  • WSL interop can be disabled (/etc/wsl.conf) for maximum security
  • SSH keys never leave the Windows SSH agent - only authentication requests are forwarded

License

MIT License - feel free to modify and distribute.

Contributing

Found a bug or have an improvement? Please open an issue or submit a pull request!

# WSL Environment Variables
# This file contains environment variables to be shared with WSL
# Note: .env files don't support WSLENV flags - all variables pass as-is
# GitHub token for repository access
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Anthropic API key for Claude access
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# OpenAI API key
OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Database connection string
DATABASE_URL=postgresql://user:password@localhost:5432/mydb
# Application environment
NODE_ENV=development
# Environment Variables - Prevent accidental commit of secrets
*.env
.env
.env.local
.env.development
.env.production
.env.staging
.env.test
# JSON configuration files containing secrets
secrets.json
*secrets*.json
config.json
*config*.json
# Backup files created by the script
wslenv_backup_*.txt
envvar_changes_*.log
# PowerShell history and temporary files
*.ps1xml
*.psd1
*.psm1.backup
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
# Folder config file
Desktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Mac system files (in case of cross-platform development)
.DS_Store
# Keep example files (these are safe templates)
!*.example
!*.template
!*-example.*
!*-template.*

WSL Environment Variables Setup Script

Easily configure environment variables for WSL access with secure handling and WSLENV management.

This PowerShell script (Set-WSLEnvironmentVariables.ps1) provides both interactive and file-based modes for setting up environment variables that are accessible in WSL environments through the WSLENV configuration.

Features

  • Two Operation Modes - Interactive setup or batch processing from files
  • Secure Value Handling - Masks sensitive tokens/keys in output (shows first/last 4 chars)
  • WSLENV Flag Support - Path conversion, case transformation, and append modes
  • Pre-seeding Logic - Reads existing WSLENV to avoid losing current configuration
  • Multiple File Formats - Supports .env and JSON configuration files
  • Error Handling - Comprehensive validation and actionable error messages
  • User-level Persistence - Variables survive reboots and are available system-wide

Quick Start

Interactive Mode (Default)

.\Set-WSLEnvironmentVariables.ps1

File Mode

# Using .env file
.\Set-WSLEnvironmentVariables.ps1 .env

# Using JSON configuration
.\Set-WSLEnvironmentVariables.ps1 secrets.json

File Formats

.env Format (Simple)

# WSL Environment Variables
GITHUB_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
ANTHROPIC_API_KEY=sk-ant-api03-xxxxxxxxxxxx
NODE_ENV=development
DATABASE_URL=postgresql://user:password@localhost:5432/mydb

Note: .env files don't support WSLENV flags - all variables pass as-is to WSL.

JSON Format (Advanced)

{
  "GITHUB_TOKEN": {
    "Value": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "Flag": ""
  },
  "DEVELOPMENT_PATH": {
    "Value": "C:\\Dev\\Tools",
    "Flag": "p"
  },
  "BUILD_ENV": {
    "Value": "development",
    "Flag": "l"
  }
}

JSON format supports WSLENV flags for advanced path and case transformations.

WSLENV Flags

Flag Purpose Example Use Case
(empty) Pass as-is GITHUB_TOKEN API keys, tokens, simple values
/p Convert Windows path to WSL path C:\Tools/mnt/c/Tools File paths, directories
/l Convert to lowercase MyValuemyvalue Case-sensitive systems
/u Convert to uppercase myvalueMYVALUE Environment standards
/up Convert path AND append to existing Add to WSL PATH PATH-like variables

Interactive Mode Workflow

The interactive mode provides a guided setup with clear visual steps:

Step 1: Review Pre-seeded Variables

  • Reviews existing WSLENV configuration
  • Shows current values (masked for security)
  • Options to keep, update, or remove each variable

Step 2: Add Additional Variables

  • Interactive loop to add new environment variables
  • Secure input for sensitive values (hidden)
  • WSLENV flag configuration for each variable

Step 3: Review Summary

  • Shows all variables with sanitized values
  • Displays WSLENV flags for verification
  • Confirmation before making changes

Step 4: Set Variables

  • Sets Windows User-level environment variables
  • Real-time feedback on success/failure

Step 5: Configure WSLENV

  • Builds WSLENV string with proper flags
  • Preserves unrelated existing WSLENV entries
  • Shows final WSLENV configuration

Security Features

Value Sanitization

Sensitive values are automatically detected and masked in all output:

  • Pattern: Shows first 4 and last 4 characters
  • Detection: Variables containing TOKEN, KEY, SECRET, or PASSWORD
  • Example: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxghp_********************************xxxx

Secure Input

  • Sensitive variables use PowerShell's -AsSecureString for hidden input
  • Values never appear in clear text during interactive sessions
  • Terminal history doesn't capture actual token values

Safe Configuration Management

  • Never overwrites existing WSLENV entries unintentionally
  • Explicit confirmation required for variable removal
  • Backup existing configuration before changes

Prerequisites

Windows

  • PowerShell 5.1 or later
  • Windows with WSL2 installed
  • User permissions to set environment variables

WSL

  • Any WSL2 distribution
  • Variables will be available after restart

Usage Examples

Set Up Development Environment

# Interactive setup for development tokens
.\Set-WSLEnvironmentVariables.ps1

# Or use a prepared configuration
.\Set-WSLEnvironmentVariables.ps1 dev-secrets.json

Team Configuration Sharing

{
  "PROJECT_ROOT": {
    "Value": "C:\\Projects\\MyApp",
    "Flag": "p"
  },
  "BUILD_ENV": {
    "Value": "development",
    "Flag": "l"
  }
}

Share the JSON structure (without actual secret values) for consistent team setups.

CI/CD Integration

# Automated setup from environment file
.\Set-WSLEnvironmentVariables.ps1 $env:BUILD_SECRETS_FILE

Verification

After setup, verify variables are available in WSL:

From PowerShell

wsl -- printenv | Select-String 'GITHUB_TOKEN|ANTHROPIC_API_KEY'

Inside WSL

# Check specific variables
echo $GITHUB_TOKEN
echo $ANTHROPIC_API_KEY

# List all configured variables
printenv | grep -E 'GITHUB_TOKEN|ANTHROPIC_API_KEY'

Restart Requirements

⚠️ Important: After running the script, restart required for changes to take effect:

  1. Close PowerShell window
  2. Open new PowerShell/Terminal session
  3. Start fresh WSL session

Why restart is needed:

  • Windows needs to reload User environment variables
  • WSL needs to read updated WSLENV configuration
  • Existing sessions won't see new variables

Troubleshooting

Variables Not Visible in WSL

  • Ensure restart was performed
  • Check WSLENV contains the variable: echo $env:WSLENV
  • Verify variable exists in Windows: [Environment]::GetEnvironmentVariable("VARNAME", "User")

File Mode Errors

  • Verify file exists and is readable
  • Check file format (JSON syntax, .env key=value pairs)
  • Ensure file encoding is UTF-8

Permission Issues

  • Run PowerShell as regular user (not administrator)
  • User-level environment variables don't require elevation
  • Check Windows environment variable limits (32KB total)

Example Files

The repository includes example configuration files:

  • .env.example - Simple environment variable format
  • secrets.json.example - Advanced JSON format with WSLENV flags

Copy and modify these templates for your specific needs.

Security Best Practices

  1. Never commit actual secrets - Use .env.example templates instead
  2. Use gitignore - Add *.env and secrets.json to .gitignore
  3. Regular rotation - Update API keys and tokens regularly
  4. Principle of least privilege - Only set variables needed for WSL
  5. Secure storage - Consider Windows Credential Manager for highly sensitive data

Advanced Usage

Multiple Environment Configurations

# Development environment
.\Set-WSLEnvironmentVariables.ps1 dev.env

# Production environment
.\Set-WSLEnvironmentVariables.ps1 prod.env

Path Variable Management

{
  "DEVELOPMENT_TOOLS": {
    "Value": "C:\\Dev\\Tools\\bin",
    "Flag": "p"
  },
  "EXTRA_PATH": {
    "Value": "C:\\CustomTools",
    "Flag": "up"
  }
}

Conditional Variable Setting

Use PowerShell logic to conditionally run:

if ($env:COMPUTERNAME -eq "DEV-MACHINE") {
    .\Set-WSLEnvironmentVariables.ps1 dev-secrets.json
} else {
    .\Set-WSLEnvironmentVariables.ps1 prod-secrets.json
}

Integration with Other Tools

Works with SSH Bridge

Complements the WSL SSH Agent Bridge script in this repository - set SSH_AUTH_SOCK alongside other environment variables for complete WSL development setup.

IDE Integration

Environment variables set by this script are available to:

  • VS Code with WSL extension
  • IntelliJ IDEA with WSL support
  • Any IDE running inside WSL

Build Systems

Variables are accessible to:

  • npm/yarn scripts running in WSL
  • Docker containers launched from WSL
  • CI/CD tools operating in WSL environment

Contributing

Found an issue or have a feature request? Please open an issue in the repository. Pull requests welcome for improvements to:

  • Additional file format support
  • Enhanced security features
  • Better error handling
  • Documentation improvements

License

MIT License - modify and distribute freely.

WSL SSH Agent Bridge Architecture

This document provides a detailed architectural overview of the WSL SSH Agent Bridge system, which enables secure SSH key sharing between Windows and WSL environments without compromising security through filesystem mounts or risky interop configurations.

Problem Statement

By default, WSL environments cannot access Windows SSH keys stored in the Windows SSH Agent. Traditional solutions require:

  • Copying private keys to WSL (security risk)
  • Enabling WSL filesystem mounts (larger attack surface)
  • Complex interop configurations (compatibility issues)

This bridge provides a secure, network-based solution that maintains isolation while enabling seamless SSH authentication.

System Architecture

%%{init: {"theme": "base"}}%%
graph LR
    subgraph Windows["🪟 Windows Host"]
        subgraph WinAuth["Authentication Layer"]
            Keys[SSH Keys<br/>id_rsa, id_ed25519]:::base3
            Agent[SSH Agent<br/>Service]:::blue
        end

        subgraph WinComm["Communication Layer"]
            Pipe[Named Pipe<br/>openssh-ssh-agent]:::cyan
        end

        subgraph WinBridge["Bridge Layer"]
            NPipe[npiperelay.exe]:::violet
            Ncat[ncat server<br/>:2626]:::magenta
        end
    end

    subgraph Network["🌐 Network Bridge"]
        TCP[TCP Connection<br/>:2626]:::orange
    end

    subgraph WSL["🐧 WSL Environment"]
        subgraph WSLBridge["Bridge Layer"]
            Socat[socat client]:::magenta
        end

        subgraph WSLComm["Communication Layer"]
            Socket[Unix Socket<br/>agent.sock]:::cyan
        end

        subgraph WSLApps["Application Layer"]
            Git[git]:::green
            SSH[ssh]:::green
            Tools[other tools]:::green
        end
    end

    %% Horizontal connections within layers
    Keys --- Agent
    Agent --- Pipe
    Pipe --- NPipe
    NPipe --- Ncat

    Git --- Socket
    SSH --- Socket
    Tools --- Socket
    Socket --- Socat

    %% Cross-boundary connections
    Ncat -.->|network| TCP
    TCP -.->|network| Socat

  %% --- Dark tones (background) ---
  classDef base03 fill:#002b36,color:#fdf6e3,stroke:#073642,stroke-width:1.2px;
  classDef base02 fill:#073642,color:#fdf6e3,stroke:#002b36,stroke-width:1.2px;
  classDef base01 fill:#586e75,color:#fdf6e3,stroke:#002b36,stroke-width:1.2px;
  classDef base00 fill:#657b83,color:#fdf6e3,stroke:#002b36,stroke-width:1.2px;

  %% --- Light tones (foreground) ---
  classDef base0 fill:#839496,color:#002b36,stroke:#073642,stroke-width:1.2px;
  classDef base1 fill:#93a1a1,color:#002b36,stroke:#073642,stroke-width:1.2px;
  classDef base2 fill:#eee8d5,color:#002b36,stroke:#586e75,stroke-width:1.2px;
  classDef base3 fill:#fdf6e3,color:#002b36,stroke:#586e75,stroke-width:1.2px;

  %% --- Accent colors ---
  classDef yellow  fill:#b58900,color:#002b36,stroke:#7a6000,stroke-width:1.2px;
  classDef orange  fill:#cb4b16,color:#002b36,stroke:#8b320f,stroke-width:1.2px;
  classDef red     fill:#dc322f,color:#002b36,stroke:#8b1f1c,stroke-width:1.2px;
  classDef magenta fill:#d33682,color:#002b36,stroke:#852455,stroke-width:1.2px;
  classDef violet  fill:#6c71c4,color:#fdf6e3,stroke:#43479a,stroke-width:1.2px;
  classDef blue    fill:#268bd2,color:#fdf6e3,stroke:#1a5f92,stroke-width:1.2px;
  classDef cyan    fill:#2aa198,color:#002b36,stroke:#1e766f,stroke-width:1.2px;
  classDef green   fill:#859900,color:#002b36,stroke:#5b6a00,stroke-width:1.2px;
Loading

Architecture Layers

Windows Host

The Windows side manages SSH authentication and provides the bridge to WSL:

  • Authentication Layer:

    • SSH Keys: Private keys (RSA, Ed25519, etc.) stored securely on Windows
    • SSH Agent Service: Windows OpenSSH agent that manages key authentication without exposing private keys
  • Communication Layer:

    • Named Pipe: Windows inter-process communication mechanism (\\.\pipe\openssh-ssh-agent) that SSH Agent uses for local communication
  • Bridge Layer:

    • npiperelay.exe: Translates between Windows named pipes and TCP sockets
    • ncat server: Network communication server listening on port 2626, handles incoming TCP connections from WSL

Network Bridge

The network layer provides secure, isolated communication:

  • TCP Connection: Uses WSL's virtual network interface (port 2626)
  • Isolation: Network traffic is contained within the Windows-WSL bridge network
  • Protocol: Maintains SSH Agent Protocol (RFC 4252) integrity across the network boundary

WSL Environment

The WSL side provides a native Unix interface for SSH applications:

  • Bridge Layer:

    • socat client: Network client that connects to Windows ncat server and translates TCP to Unix domain sockets
  • Communication Layer:

    • Unix Socket: Standard Unix domain socket ($HOME/.ssh/agent.sock) that SSH applications expect
  • Application Layer:

    • git: Version control with SSH authentication for repositories
    • ssh: Direct SSH connections to remote servers
    • other tools: Any application that uses SSH authentication (rsync, scp, etc.)

Data Flow Explanation

The bridge operates as a secure relay system that maintains the SSH Agent Protocol throughout:

  1. Application Request: SSH applications in WSL (git, ssh) need authentication and connect to the Unix socket ($HOME/.ssh/agent.sock)

  2. Socket to Network: socat receives the request and forwards it over TCP to the Windows host (IP:2626)

  3. Network to Pipe: ncat server on Windows receives the TCP request and forwards it to npiperelay

  4. Pipe to Agent: npiperelay translates the request to Windows named pipe format and sends it to the SSH Agent

  5. Authentication: SSH Agent processes the authentication request using the loaded private keys

  6. Response Chain: The authentication response follows the same path in reverse:

    • SSH Agent → Named Pipe → npiperelay → ncat → TCP → socat → Unix Socket → Application

Security Benefits

This architecture provides several security advantages:

  • No Private Key Exposure: Private keys never leave the Windows SSH Agent
  • No Filesystem Mounts: WSL doesn't need access to Windows filesystems
  • Network Isolation: TCP traffic is limited to the WSL-Windows bridge interface
  • Process Isolation: Each component runs in its own security context
  • Protocol Integrity: SSH Agent Protocol is maintained end-to-end without modification

Network Security

The TCP connection operates over WSL's virtual network adapter, which:

  • Is isolated from external networks
  • Only allows communication between Windows host and WSL instances
  • Uses a dedicated port (2626) for this specific bridge service
  • Can be monitored and controlled through Windows firewall rules

This architecture enables secure SSH key sharing while maintaining the security isolation principles that make WSL a safe development environment.


ADR: Use a TCP relay (Windows ncat + npiperelay) and WSL socat to expose the Windows ssh-agent into WSL

Status: Accepted Date: 2025-09-13 Decision Owner: @cprima

Context

WSL shells (any user, any distro; specifically NixOS-WSL) must use keys already loaded in the Windows OpenSSH agent without copying private keys into WSL and without broad, fragile cross-boundary dependencies (e.g., full drive mounts, PATH pollution, or brittle interop invocation semantics). The solution must:

  • Work reliably with interop and auto-mount either off or minimized.
  • Be distro-agnostic on the WSL side and non-intrusive on Windows.
  • Provide a single, stable Unix socket in WSL (/run/ssh-agent.sock) for all users.
  • Self-heal if the Windows side starts later, and avoid log spam.

Decision

Adopt a network bridge:

  • Windows (host): listen on the WSL vEthernet IP (e.g., 172.22.0.1) using ncat -l <WSL_IP> 2626 -k -e "npiperelay.exe -ei -s \\.\pipe\openssh-ssh-agent" This translates TCP ⇄ Windows named pipe (OpenSSH agent).

  • WSL (guest): run a systemd service that creates and maintains socat UNIX-LISTEN:/run/ssh-agent.sock,fork,mode=0666,unlink-early TCP:<host_ip>:2626 where <host_ip> is the default gateway in WSL (ip route | awk '/^default via/{print $3; exit}').

All shells pick up SSH_AUTH_SOCK=/run/ssh-agent.sock via /etc/profile.d.

Why this architecture

  • Robustness: avoids brittle EXEC of Windows binaries from WSL (quoting/escaping, argv parsing, drvfs path issues, and binfmt/interop assumptions).
  • Security: no key copies, only agent protocol over a localhost-scoped TCP path bound to the WSL vEthernet interface; no broad drive mounts required.
  • Simplicity: clean separation of concerns—Windows owns the named pipe; WSL owns the Unix socket; TCP is the glue.
  • Distro-agnostic: only socat needed in WSL.
  • Multi-user: systemd service exposes a single /run/ssh-agent.sock for all users.

Consequences / Trade-offs

  • Requires ncat on Windows (from Nmap) and npiperelay.exe.
  • Relies on a long-running Windows listener (provided via a simple startup script / scheduled task).
  • Uses a TCP hop (localhost segment via vEthernet). Binding to the WSL adapter IP limits exposure.

Implementation summary (current)

  • Windows:

    • Ensure ssh-agent is running and has identities (ssh-add -l, add if empty).
    • Start: ncat -l <WSL_IP> 2626 -k -e "C:\...\npiperelay.exe -ei -s \\.\pipe\openssh-ssh-agent"
    • Optionally wrap in a PowerShell script and schedule at user logon.
  • WSL / NixOS:

    • systemd service runs a tiny script that computes <host_ip> (default gateway) and spawns: socat UNIX-LISTEN:/run/ssh-agent.sock,fork,mode=0666,unlink-early TCP:<host_ip>:2626
    • /etc/profile.d/20-ssh-agent-bridge.sh: export SSH_AUTH_SOCK=/run/ssh-agent.sock if the socket exists.
  • Validation:

    • In WSL: ssh-add -l → lists Windows keys; ssh -T [email protected] → success banner.

What did not work (do not attempt again)

  1. socat EXEC:<windows.exe + args> from WSL

    • Repeated EXEC: wrong number of parameters (2 instead of 1) from socat.
    • Argument parsing and quoting/escaping of Windows paths/flags routinely failed.
  2. Calling Windows binaries from WSL via drvfs paths (e.g., O:\WSL\...npiperelay.exe)

    • command not found or Invalid argument from npiperelay.exe.
    • Subtle differences in argv handling and path translation.
  3. Wrapping .cmd or .bat and running via socat EXEC

    • WSL invoked them under bash, not cmd.exe; resulted in @echo: command not found and similar.
  4. Relying on cmd.exe presence from WSL

    • With interop.appendWindowsPath=false or interop tweaks, cmd.exe was missing; UNC path warnings (UNC paths are not supported) also surfaced.
  5. Direct pipe names in socat (trying \\.\pipe\openssh-ssh-agent from WSL)

    • Not a supported WSL address target; npiperelay must run on Windows.
  6. Mis-binding the Windows listener

    • Binding to 127.0.0.1 while WSL connected to 10.255.255.254 (or vice-versa) → connection refused.
    • The correct binding is the WSL vEthernet IP (Get-NetIPConfiguration | ? InterfaceAlias -like '*WSL*').
  7. Wrong pipe name (e.g., ssh-pageant) when using Windows OpenSSH agent

    • Use \\.\pipe\openssh-ssh-agent unless intentionally bridging a different agent.
  8. Expecting WSL success when Windows side isn't up

    • Leads to communication with agent failed. The network listener must be started first (or WSL retries).

Risks & mitigations

  • Windows listener not started yet → WSL ssh-add -l fails. Mitigation: systemd service keeps socket available; start listener on Windows logon; WSL will succeed once Windows is ready.

  • Firewall changes → blocked local TCP on vEthernet. Mitigation: bind to WSL adapter IP; allow local profile inbound on port 2626 if policy requires.

  • Log noise if Windows side flaps. Mitigation: systemd Restart=always, RestartSec=2; default journal verbosity is acceptable; no TTY spam.

Security considerations

  • No private keys cross boundaries; only SSH agent protocol proxied.
  • TCP listener is bound to WSL vEthernet IP, not all interfaces.
  • WSL socket is world-readable (0666) by design to serve all users; adjust mode if needed.

Alternatives considered

  • WSL interop + direct npiperelay exec: brittle quoting/argv; fails under tighter interop settings.
  • Drive-mounted npiperelay.exe from WSL: path/argv issues; depends on drvfs mounts; fragile.
  • wsl-ssh-pageant: alternative agent, but adds another component and pipe; current need is Windows OpenSSH agent.

Operational notes

  • Windows prerequisites: OpenSSH Client, ssh-agent running with keys, ncat.exe, npiperelay.exe.
  • WSL prerequisites: socat.
  • Typical IPs: Windows vEthernet (host) ~ 172.22.0.1; WSL gets default route via that IP.

Acceptance criteria

  • ssh-add -l in WSL lists Windows keys.
  • ssh -T [email protected] succeeds.
  • Works across reboots with Windows logon task + WSL systemd service enabled.
  • No key material stored in WSL; no broad mounts or PATH pollution required.

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

This repository contains two main PowerShell utilities for WSL development:

  1. Manage-WslSshBridge.ps1 - A PowerShell security utility for creating secure SSH agent bridges between Windows and WSL environments. The script enables WSL to use SSH keys from the Windows SSH agent while maintaining security isolation.

  2. Set-WSLEnvironmentVariables.ps1 - An environment variable management script with both interactive and file-based modes for securely configuring WSL-accessible environment variables with WSLENV support.

SSH Bridge Architecture (Manage-WslSshBridge.ps1)

The SSH bridge implements a secure network-based architecture:

  • Windows side: OpenSSH agent with named pipes (\\.\pipe\openssh-ssh-agent)
  • Bridge components: npiperelay + socat/ncat for protocol translation
  • WSL side: Unix domain socket ($HOME/.ssh/agent.sock)
  • Network relay: TCP connection between Windows host and WSL on port 2626

Security Levels

Maximum Security (Preferred):

  • Uses WSL-native npiperelay (/usr/local/bin/npiperelay)
  • Minimal Windows dependencies (pipe access only)
  • Supports operation with WSL interop disabled

Fallback Security:

  • Uses npiperelay from Windows filesystem via WSL mounts
  • Requires WSL interop and drive mounting enabled

Key Components

Core Functions

  • Start-WslSshBridge: Creates bridge with comprehensive validation
  • Stop-WslSshBridge: Cleanly shuts down bridge processes
  • Find-NpiperelayCurrent: Auto-detects npiperelay installations

Error Handling

  • Specific exit codes for automation ($ExitCodes hashtable)
  • Actionable error messages with remediation steps
  • Smoke tests verify bridge functionality after startup

Dependencies

  • Windows: OpenSSH client, ssh-agent service, npiperelay.exe, ncat.exe
  • WSL: socat package, target distribution with SSH support

Environment Variables Management (Set-WSLEnvironmentVariables.ps1)

Operation Modes

  • Interactive Mode: Guided setup with visual workflow steps
  • File Mode: Batch processing from .env or JSON configuration files

Key Features

  • Secure value sanitization (shows first/last 4 chars of tokens)
  • WSLENV flag support for path conversion and case transformation
  • Pre-seeding from existing WSLENV configuration
  • Multiple file format support (.env, JSON)
  • User-level persistence across reboots

Common Commands

SSH Bridge Usage

# Restart bridge (default action)
.\Manage-WslSshBridge.ps1

# Start bridge with specific distro
.\Manage-WslSshBridge.ps1 -Action Start -Distro "Ubuntu-22.04"

# Stop bridge
.\Manage-WslSshBridge.ps1 -Action Stop

Environment Variables Usage

# Interactive mode
.\Set-WSLEnvironmentVariables.ps1

# File mode with .env
.\Set-WSLEnvironmentVariables.ps1 .env

# File mode with JSON
.\Set-WSLEnvironmentVariables.ps1 secrets.json

Validation Commands

# Check SSH bridge is working
wsl -d DistroName -- bash -lc 'SSH_AUTH_SOCK="$HOME/.ssh/agent.sock" ssh-add -l'

# Check environment variables in WSL
wsl -- printenv | grep -E 'GITHUB_TOKEN|ANTHROPIC_API_KEY'

WSL Setup for Persistent Bridge

For a permanent, system-wide WSL configuration that automatically starts the SSH bridge:

WSL Configuration

# /etc/wsl.conf - Secure WSL configuration
[interop]
enabled = false
appendWindowsPath = false

[automount]
enabled = false

System Service Setup

Create a systemd service that automatically starts the SSH bridge:

# Bridge script (adapt paths for your distribution)
#!/bin/bash
set -euo pipefail
SOCK=/run/ssh-agent.sock
HOST="$(ip route | awk '/^default via/{print $3; exit}')"
rm -f "$SOCK" || true
exec socat UNIX-LISTEN:"$SOCK",fork,mode=0666,unlink-early TCP:"$HOST":2626
# /etc/systemd/system/wsl-ssh-agent-bridge.service
[Unit]
Description=WSL to Windows SSH Agent Bridge
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
ExecStart=/path/to/wsl-ssh-bridge-script
Restart=always
RestartSec=2
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target

Global Environment Setup

# /etc/profile.d/ssh-agent-bridge.sh - Auto-export for all users
[ -S /run/ssh-agent.sock ] && export SSH_AUTH_SOCK=/run/ssh-agent.sock

Installation Commands

# Enable and start the service
sudo systemctl enable wsl-ssh-agent-bridge.service
sudo systemctl start wsl-ssh-agent-bridge.service

# Verify status
sudo systemctl status wsl-ssh-agent-bridge.service

Testing and Validation

SSH Bridge

The SSH bridge script includes built-in smoke tests:

  • Socket existence verification
  • SSH agent connectivity validation
  • Automated error detection with specific remediation guidance
  • No external test framework used - validation is integrated

Environment Variables

The environment variable script includes:

  • Secure value sanitization in all output
  • Pre-setup validation of existing WSLENV
  • Post-setup verification commands for both Windows and WSL
  • Configuration file format validation (.env and JSON)

File Structure

  • Manage-WslSshBridge.ps1 - SSH agent bridge utility
  • Set-WSLEnvironmentVariables.ps1 - Environment variable management
  • architecture-diagram.md - Technical architecture documentation with embedded ADR
  • README-EnvVars.md - Comprehensive environment variables documentation
  • .env.example - Template for environment variables
  • secrets.json.example - Template for JSON configuration
  • TODO.md - Development roadmap and completed features
  • .gitignore - Protects against accidental secret commits
<#
.SYNOPSIS
Secure WSL SSH agent bridge for sharing SSH keys between Windows and WSL without drive mounts.
.DESCRIPTION
This script creates a secure bridge between Windows OpenSSH agent and WSL environments
using ncat and socat over TCP. It enables WSL to use SSH keys from the Windows SSH agent
while maintaining security isolation.
The script supports three operations:
- Start: Creates the bridge and validates connectivity with smoke tests
- Stop: Cleanly shuts down bridge processes and removes sockets
- Restart: Stops then starts the bridge (default)
Network Architecture:
- Windows: ncat server listening on WSL network interface (port 2626)
- WSL: socat client connecting Unix socket to TCP endpoint
- Protocol: SSH Agent Protocol (RFC 4252) over TCP tunnel
- Security: Network isolated to WSL-Windows bridge interface
Key features:
- Network-based bridge avoids filesystem mount dependencies
- Comprehensive smoke tests verify bridge functionality
- Specific exit codes for automation and debugging
- Actionable error messages with remediation steps
- Auto-detects WSL network configuration and adapters
.PARAMETER Action
The operation to perform: Start, Stop, or Restart (default: Restart)
.PARAMETER Distro
WSL distribution name to use (default: NixOS-golden)
.PARAMETER WinKey
Path(s) to Windows SSH private key(s) (default: $env:USERPROFILE\.ssh\id_rsa)
Can specify multiple keys: -WinKey "key1.pem","key2.pem" or multiple parameters: -WinKey "key1.pem" -WinKey "key2.pem"
.PARAMETER NpPath
Path to npiperelay.exe for ncat server backend (default: auto-detected from script directory, then common locations)
.PARAMETER PipeName
Windows named pipe to connect to (default: openssh-ssh-agent)
.EXAMPLE
.\Manage-WslSshBridge.ps1
Restarts the SSH bridge using network relay
.EXAMPLE
.\Manage-WslSshBridge.ps1 -Action Start -Distro "Ubuntu-22.04"
Starts the bridge using Ubuntu-22.04 distro
.EXAMPLE
.\Manage-WslSshBridge.ps1 -WinKey "$env:USERPROFILE\.ssh\id_rsa","$env:USERPROFILE\.ssh\id_ed25519"
Loads multiple SSH keys into the agent before starting the bridge
.EXAMPLE
.\Manage-WslSshBridge.ps1 -WinKey "C:\keys\work.pem" -WinKey "C:\keys\personal.pem"
Alternative syntax for loading multiple keys
.EXAMPLE
.\Manage-WslSshBridge.ps1 -Action Stop
Stops the SSH bridge and cleans up all processes
.NOTES
Network Security:
- TCP connection limited to WSL-Windows bridge interface
- No filesystem mounts or interop required in WSL
- SSH Agent Protocol tunneled over secure local network
- Windows ncat server handles named pipe to TCP translation
Requirements:
- Windows OpenSSH client and ssh-agent service running
- WSL2 distribution with socat installed
- ncat (from nmap package) for Windows TCP server
- npiperelay binary (build from source: go install github.com/jstarks/npiperelay@latest)
- SSH key loaded in Windows SSH agent
Protocol: Uses standard SSH Agent Protocol (RFC 4252) over TCP tunnel
Bridge: WSL Unix socket ↔ socat ↔ TCP:2626 ↔ ncat ↔ npiperelay ↔ Windows named pipe ↔ SSH agent
Author: Enhanced from original Start-WslSshBridge.ps1
Version: 2.1
License: MIT
#>
[CmdletBinding()]
param(
[ValidateSet("Start", "Stop", "Restart")]
[string]$Action = "Restart",
[string]$Distro = 'NixOS-golden',
[string[]]$WinKey = @("$env:USERPROFILE\.ssh\id_rsa"),
[string]$NpPath = '',
[string]$PipeName = 'openssh-ssh-agent'
)
# Exit codes
$ExitCodes = @{
Success = 0
SshAgentFailed = 1
PrereqMissing = 2
WslError = 3
SshAddFailed = 4
SocketFailed = 5
SocatMissing = 6
InteropDisabled = 7
MountMissing = 8
}
function Die($m, $exitCode = 1){ Write-Error $m; exit $exitCode }
function Say($m){ Write-Host $m }
function Find-NpiperelayCurrent {
$paths = @(
'C:\ProgramData\chocolatey\bin\npiperelay.exe',
'C:\tools\npiperelay\npiperelay.exe',
"$env:USERPROFILE\scoop\apps\npiperelay\current\npiperelay.exe",
"$env:USERPROFILE\AppData\Local\Microsoft\WinGet\Packages\jstarks.npiperelay_Microsoft.Winget.Source_8wekyb3d8bbwe\npiperelay.exe"
)
foreach ($path in $paths) {
if (Test-Path $path) { return $path }
}
return $null
}
function Stop-WslSshBridge {
Say "Stopping WSL SSH Bridge..."
# Kill Windows processes (like original reset)
taskkill /IM wsl-ssh-pageant.exe /F 2>$null | Out-Null
taskkill /IM npiperelay.exe /F 2>$null | Out-Null
# Stop WSL side if distro is available
if ($script:EffectiveDistro) {
$stopScript = @'
#!/usr/bin/env bash
SOCK="$HOME/.ssh/agent.sock"
pkill -f "socat.*npiperelay.*openssh-ssh-agent" 2>/dev/null || true
pkill -f "npiperelay.*openssh-ssh-agent" 2>/dev/null || true
rm -f "$SOCK"
echo "WSL bridge stopped"
'@
$stopLF = $stopScript -replace "`r", ""
$stopB64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($stopLF))
$stopCmd = "echo '$stopB64' | base64 -d | bash"
try {
$result = wsl -d $script:EffectiveDistro -- bash -lc "$stopCmd" 2>&1
Say "WSL bridge processes stopped"
} catch {
Say "WSL bridge may not have been running"
}
}
}
function Start-WslSshBridge {
# Exact copy of original script logic, just inside a function
# --- RESET WINDOWS ---
taskkill /IM wsl-ssh-pageant.exe /F 2>$null | Out-Null
taskkill /IM npiperelay.exe /F 2>$null | Out-Null
try { Stop-Service ssh-agent -ErrorAction SilentlyContinue } catch {}
try { Start-Service ssh-agent -ErrorAction Stop } catch { Die 'Cannot start ssh-agent as user (service ACL).' $ExitCodes.SshAgentFailed }
# Ensure keys in agent
foreach ($keyPath in $script:EffectiveWinKeys) {
if (-not (Test-Path $keyPath)) { Die "Key not found: $keyPath" $ExitCodes.PrereqMissing }
}
$ids = & ssh-add -l 2>$null
if ($LASTEXITCODE -ne 0 -or -not $ids -or $ids -match 'no identities') {
foreach ($keyPath in $script:EffectiveWinKeys) {
Say "Loading SSH key: $keyPath"
& ssh-add $keyPath | Out-Null
}
}
$ids = & ssh-add -l 2>$null
if ($LASTEXITCODE -ne 0) { Die 'ssh-add -l failed on Windows.' $ExitCodes.SshAddFailed }
Say "Windows agent identities:`n$ids"
# --- PREREQS ---
if (-not (Get-Command ssh -ErrorAction SilentlyContinue)) { Die 'OpenSSH client missing (ssh not on PATH).' }
# Ensure OpenSSH pipe exists
$hasPipe = (Get-ChildItem \\.\pipe\ 2>$null | ? Name -eq $PipeName)
if (-not $hasPipe) {
& ssh -G localhost *>$null 2>&1 | Out-Null
Start-Sleep -Milliseconds 200
$hasPipe = (Get-ChildItem \\.\pipe\ 2>$null | ? Name -eq $PipeName)
}
if (-not $hasPipe) { Die "Named pipe \\.\pipe\$PipeName not found. Repair OpenSSH Client or re-login to Windows." }
# --- WSL CHECKS ---
$distros = @(wsl -l -q | % { $_.Trim() } | ? { $_ })
if (-not ($distros -contains $script:EffectiveDistro)) { Die "WSL distro '$script:EffectiveDistro' not found. Available: $($distros -join ', ')" }
# Check WSL interop status (binfmt_misc shows WSLInterop when enabled)
$interopCheck = (wsl -d $script:EffectiveDistro -- bash -lc 'ls /proc/sys/fs/binfmt_misc/ 2>/dev/null | grep -q WSLInterop && echo ENABLED || echo DISABLED' 2>&1 | Out-String).Trim()
$interopWorking = ($interopCheck -eq 'ENABLED')
if ($interopWorking) {
Say "WSL interop enabled - required for pipe access."
} else {
Say "WSL interop disabled - this is good for security. Will only use WSL-native npiperelay."
}
# Find ncat.exe for network relay approach
$ncat = (Get-Command ncat.exe -EA SilentlyContinue).Source
if (-not $ncat) {
$ncatPaths = @(
'C:\Program Files\Nmap\ncat.exe',
'C:\Program Files (x86)\Nmap\ncat.exe',
'C:\ProgramData\chocolatey\lib\nmap\tools\ncat.exe'
)
$ncat = $ncatPaths | Where-Object {Test-Path $_} | Select-Object -First 1
}
if (-not $ncat) {
Die "ncat.exe not found. Install nmap: choco install nmap"
}
# Also need npiperelay.exe for Windows side
if (-not (Test-Path $script:EffectiveNpPath)) {
Die "npiperelay.exe not found at: $script:EffectiveNpPath (needed for Windows ncat server)"
}
# Get WSL host IP
$wslAdapter = Get-NetIPConfiguration | Where-Object InterfaceAlias -like '*WSL*'
$wslIP = $wslAdapter.IPv4Address.IPAddress | Select-Object -First 1
if (-not $wslIP) {
Die "WSL host IP not found. Ensure WSL2 is running."
}
Say "Using network relay: ncat on $wslIP:2626"
# --- BASH PAYLOAD (LF + BASE64; simple, no nested quoting) ---
$bashScript = @'
#!/usr/bin/env bash
set -e
SOCK="$HOME/.ssh/agent.sock"
command -v socat >/dev/null 2>&1 || { echo "ERR:NO_SOCAT"; exit 6; }
# Get Windows host IP (WSL2 default gateway)
HOST=$(ip route | awk '/^default via/{print $3; exit}')
[ -n "$HOST" ] || { echo "ERR:NO_HOST"; exit 7; }
rm -f "$SOCK"; mkdir -p "$HOME/.ssh"
# Start network bridge (TCP to Unix socket)
pkill socat 2>/dev/null || true
setsid nohup socat UNIX-LISTEN:"$SOCK",fork TCP:$HOST:2626 >/dev/null 2>&1 &
# Wait for socket
for i in 1 2 3 4 5; do [ -S "$SOCK" ] && break; sleep 0.25; done
[ -S "$SOCK" ] || { echo "ERR:NO_SOCKET"; exit 5; }
# Verify
SSH_AUTH_SOCK="$SOCK" ssh-add -l >/dev/null 2>&1 || {
echo "ERR:SSH_ADD:$(SSH_AUTH_SOCK="$SOCK" ssh-add -l 2>&1 | head -1)";
exit 4;
}
echo "OK $SOCK"
'@
# Start Windows ncat server first
Get-Process ncat -EA SilentlyContinue | Stop-Process -Force
$ncatCmd = "`"$script:EffectiveNpPath -ei -s \\.\pipe\$PipeName`""
$ncatArgs = @('-l', $wslIP, '2626', '-k', '-e', $ncatCmd)
Say "Starting ncat server: $ncat $($ncatArgs -join ' ')"
Start-Process -FilePath $ncat -ArgumentList $ncatArgs -WindowStyle Hidden
Start-Sleep -Milliseconds 500 # Let ncat start
$bashLF = $bashScript -replace "`r",""
$b64 = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($bashLF))
$cmd = "echo $b64 | base64 -d > /tmp/wsl-ssh-bridge.sh && chmod +x /tmp/wsl-ssh-bridge.sh && /tmp/wsl-ssh-bridge.sh"
$raw = wsl -d $script:EffectiveDistro -- bash -lc "$cmd" 2>&1
$rc = $LASTEXITCODE
$out = ($raw | Out-String).Trim()
if ($rc -eq 0 -and $out -match '^OK ') {
$sock = ($out -split ' ')[1]
Say "Bridge up."
# Smoke tests
Say "`nRunning smoke tests..."
$tests = @(
@{
Name = "Socket exists"
Command = "[ -S '$sock' ] && echo OK || echo FAIL"
Expected = "OK"
},
@{
Name = "SSH agent responds"
Command = "SSH_AUTH_SOCK='$sock' ssh-add -l >/dev/null 2>&1 && echo OK || echo FAIL"
Expected = "OK"
}
)
foreach ($test in $tests) {
$result = wsl -d $script:EffectiveDistro -- bash -lc "$($test.Command)" 2>&1 | Out-String
$status = if ($result.Trim() -eq $test.Expected) { "PASS" } else { "FAIL" }
Say " [$status] $($test.Name)"
}
Say "`nUsage in WSL:"
Say " export SSH_AUTH_SOCK=$sock"
Say " ssh-add -l"
Say " ssh -T [email protected]"
Say "`nTo persist across WSL sessions, add to ~/.bashrc or /etc/profile.d/:"
# Use single quotes to prevent PowerShell variable expansion
$persist = @'
# WSL SSH Agent Bridge - Add to ~/.bashrc or /etc/profile.d/ssh-agent-bridge.sh
SOCK="$HOME/.ssh/agent.sock"
if [ ! -S "$SOCK" ]; then
rm -f "$SOCK"
# Secure: uses WSL-native npiperelay package (no interop/mounts required)
setsid nohup socat UNIX-LISTEN:"$SOCK",fork \
EXEC:'npiperelay -ei -s //./pipe/openssh-ssh-agent' \
>/dev/null 2>&1 &
# Wait briefly for socket
for i in 1 2 3; do [ -S "$SOCK" ] && break; sleep 0.1; done
fi
[ -S "$SOCK" ] && export SSH_AUTH_SOCK="$SOCK"
'@
Write-Host $persist
exit 0
}
# Handle specific errors with actionable remediation
$errorMap = @{
"ERR:NO_SOCAT" = @{
Code = $ExitCodes.SocatMissing
Message = "socat not found in WSL distro '$script:EffectiveDistro'"
Help = @(
"Install socat in your WSL distro:",
" Ubuntu/Debian: sudo apt install socat",
" NixOS: Add 'socat' to environment.systemPackages, then: sudo nixos-rebuild switch",
" Alpine: sudo apk add socat",
" Arch: sudo pacman -S socat"
)
}
"ERR:NO_SOCKET" = @{
Code = $ExitCodes.SocketFailed
Message = "Failed to create Unix socket"
Help = @(
"Debug with foreground socat (two WSL shells):",
" Shell A: socat -d -d UNIX-LISTEN:`$HOME/.ssh/agent.sock,fork EXEC:'/usr/local/bin/npiperelay -ei -s //./pipe/$PipeName'",
" Shell B: export SSH_AUTH_SOCK=`$HOME/.ssh/agent.sock; ssh-add -l",
"Check npiperelay path and pipe accessibility"
)
}
"ERR:SSH_ADD" = @{
Code = $ExitCodes.SshAddFailed
Message = "SSH agent connection test failed"
Help = @(
"Verify Windows SSH agent has keys:",
" ssh-add -l # must list your key",
" ssh-add `"path/to/your/key`" # if empty",
"Restart SSH agent: Restart-Service ssh-agent"
)
}
}
foreach ($errorKey in $errorMap.Keys) {
if ($out -match $errorKey) {
$error = $errorMap[$errorKey]
Write-Warning "Bridge FAILED: $($error.Message)"
Write-Host ""
foreach ($helpLine in $error.Help) {
Write-Host $helpLine
}
exit $error.Code
}
}
Write-Warning "Bridge FAILED (rc=$rc): $out"
exit $ExitCodes.WslError
}
# --- MAIN LOGIC ---
# Resolve defaults
$script:EffectiveDistro = $Distro
if (-not $NpPath) {
# Check for local copy first
$localCopy = Join-Path $PSScriptRoot "npiperelay.exe"
if (Test-Path $localCopy) {
$script:EffectiveNpPath = $localCopy
Say "Using local npiperelay: $script:EffectiveNpPath"
} else {
$script:EffectiveNpPath = Find-NpiperelayCurrent
if (-not $script:EffectiveNpPath) {
# No Windows fallback found - this is OK if WSL-native exists
$script:EffectiveNpPath = $localCopy # Use expected path for error messages
Say "No Windows npiperelay found - will check for WSL-native version"
} else {
Say "Found npiperelay: $script:EffectiveNpPath"
}
}
} else {
$script:EffectiveNpPath = $NpPath
if (-not (Test-Path $script:EffectiveNpPath)) {
Say "WARNING: Specified npiperelay not found at: $script:EffectiveNpPath"
}
}
$script:EffectiveWinKeys = $WinKey
# Execute action
switch ($Action) {
"Stop" {
Stop-WslSshBridge
Say "WSL SSH Bridge stopped."
}
"Start" {
Start-WslSshBridge
}
"Restart" {
Stop-WslSshBridge
Start-Sleep -Seconds 1
Start-WslSshBridge
}
}
{
"GITHUB_TOKEN": {
"Value": "ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Flag": ""
},
"ANTHROPIC_API_KEY": {
"Value": "sk-ant-api03-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"Flag": ""
},
"DEVELOPMENT_PATH": {
"Value": "C:\\Dev\\Tools",
"Flag": "p"
},
"BUILD_ENV": {
"Value": "development",
"Flag": "l"
}
}
<#
.SYNOPSIS
Interactive PowerShell script to set environment variables for WSL access.
.DESCRIPTION
This script allows setting multiple environment variables that will be accessible in WSL.
It's pre-seeded with GITHUB_TOKEN and prompts for additional key-value pairs.
Each variable can have WSLENV flags applied for path conversion or case transformation.
.EXAMPLE
.\Set-WSLEnvironmentVariables.ps1
Runs in interactive mode for setting environment variables
.EXAMPLE
.\Set-WSLEnvironmentVariables.ps1 secrets.json
Runs in file mode using JSON configuration
.EXAMPLE
.\Set-WSLEnvironmentVariables.ps1 .env
Runs in file mode using .env file
.NOTES
WSLENV Flags:
- /p : Convert Windows path to WSL path
- /l : Convert to lowercase
- /u : Convert to uppercase
- /up : Convert path and append to existing value
- (none) : Pass as-is
#>
[CmdletBinding()]
param(
[Parameter(Position=0)]
[string]$ConfigFile
)
function Get-SanitizedValue {
param(
[string]$VariableName,
[string]$Value,
[int]$ShowChars = 4
)
if ([string]::IsNullOrEmpty($Value)) {
return "(not set)"
}
if ($VariableName.Contains("TOKEN") -or $VariableName.Contains("KEY") -or $VariableName.Contains("SECRET") -or $VariableName.Contains("PASSWORD")) {
if ($Value.Length -le ($ShowChars * 2)) {
# Value too short, just mask it all
return "*" * $Value.Length
} else {
# Show first and last N chars with asterisks in between
$firstChars = $Value.Substring(0, $ShowChars)
$lastChars = $Value.Substring($Value.Length - $ShowChars, $ShowChars)
$middleLength = $Value.Length - ($ShowChars * 2)
return "$firstChars$('*' * $middleLength)$lastChars"
}
}
return $Value
}
Write-Host "WSL Environment Variable Setup" -ForegroundColor Cyan
Write-Host "=============================" -ForegroundColor Cyan
Write-Host ""
# Determine mode: Interactive vs File-based
$isInteractiveMode = [string]::IsNullOrEmpty($ConfigFile)
if ($isInteractiveMode) {
Write-Host "Running in INTERACTIVE mode" -ForegroundColor Yellow
Write-Host ""
# Interactive mode: Check existing WSLENV to determine pre-seeded variables
$currentWSLENV = [Environment]::GetEnvironmentVariable("WSLENV", "User")
$envVars = @{}
if ([string]::IsNullOrEmpty($currentWSLENV)) {
# No existing WSLENV - start with GITHUB_TOKEN
Write-Host "No existing WSLENV found. Starting with GITHUB_TOKEN as default." -ForegroundColor Yellow
$envVars["GITHUB_TOKEN"] = @{ Value = ""; Flag = "" }
} else {
# Parse existing WSLENV and pre-seed with those variables
Write-Host "Found existing WSLENV: $currentWSLENV" -ForegroundColor Green
Write-Host "Pre-seeding with existing environment variables..." -ForegroundColor Green
Write-Host ""
$existingVars = $currentWSLENV -split ":"
foreach ($varEntry in $existingVars) {
if (![string]::IsNullOrEmpty($varEntry)) {
$parts = $varEntry -split "/"
$varName = $parts[0]
$varFlag = if ($parts.Length -gt 1) { $parts[1] } else { "" }
# Get current value from Windows environment
$currentValue = [Environment]::GetEnvironmentVariable($varName, "User")
$envVars[$varName] = @{
Value = if ($currentValue) { $currentValue } else { "" }
Flag = $varFlag
}
}
}
}
} else {
Write-Host "Running in FILE mode: $ConfigFile" -ForegroundColor Yellow
Write-Host ""
# File mode: Load configuration from file
if (-not (Test-Path $ConfigFile)) {
Write-Host "Configuration file not found: $ConfigFile" -ForegroundColor Red
exit 1
}
$envVars = @{}
$fileExtension = [System.IO.Path]::GetExtension($ConfigFile).ToLower()
try {
switch ($fileExtension) {
'.json' {
Write-Host "Loading JSON configuration..." -ForegroundColor Green
$config = Get-Content $ConfigFile -Raw | ConvertFrom-Json
foreach ($property in $config.PSObject.Properties) {
$varName = $property.Name
$varConfig = $property.Value
if ($varConfig -is [string]) {
# Simple string value
$envVars[$varName] = @{ Value = $varConfig; Flag = "" }
} else {
# Object with value and flag
$envVars[$varName] = @{
Value = if ($varConfig.Value) { $varConfig.Value } else { "" }
Flag = if ($varConfig.Flag) { $varConfig.Flag } else { "" }
}
}
}
}
'.env' {
Write-Host "Loading .env configuration..." -ForegroundColor Green
$lines = Get-Content $ConfigFile
foreach ($line in $lines) {
if ($line -match '^\s*#' -or [string]::IsNullOrWhiteSpace($line)) {
continue # Skip comments and empty lines
}
if ($line -match '^([^=]+)=(.*)$') {
$varName = $matches[1].Trim()
$varValue = $matches[2].Trim()
# Remove quotes if present
if ($varValue -match "^[`"'](.*)[`"']$") {
$varValue = $matches[1]
}
$envVars[$varName] = @{ Value = $varValue; Flag = "" }
}
}
}
default {
Write-Host "Unsupported file format: $fileExtension" -ForegroundColor Red
Write-Host "Supported formats: .json, .env" -ForegroundColor Yellow
exit 1
}
}
Write-Host "Loaded $($envVars.Count) variables from $ConfigFile" -ForegroundColor Green
} catch {
Write-Host "Error reading configuration file: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
}
if ($isInteractiveMode) {
Write-Host "WSLENV Flag Options:" -ForegroundColor Yellow
Write-Host " (empty) - Pass as-is (use for: API keys, tokens, simple values)"
Write-Host " Example: GITHUB_TOKEN, ANTHROPIC_API_KEY"
Write-Host ""
Write-Host " /p - Convert Windows path to WSL path (use for: file paths, directories)"
Write-Host " Example: C:\Tools becomes /mnt/c/Tools"
Write-Host ""
Write-Host " /l - Convert to lowercase (use for: case-sensitive systems)"
Write-Host " Example: MyValue becomes myvalue"
Write-Host ""
Write-Host " /u - Convert to uppercase (use for: environment standards)"
Write-Host " Example: myvalue becomes MYVALUE"
Write-Host ""
Write-Host " /up - Convert path AND add to existing WSL variable (use for: PATH-like variables)"
Write-Host " Example: Add Windows path to existing WSL PATH variable"
Write-Host ""
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ STEP 1: Review Pre-seeded Variables ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
foreach ($varName in @($envVars.Keys)) { # Create array copy to avoid modification during enumeration
$var = $envVars[$varName]
$currentValue = $var.Value
$flagText = if ([string]::IsNullOrEmpty($var.Flag)) { "(no flag)" } else { "/$($var.Flag)" }
Write-Host ""
Write-Host "Variable: $varName $flagText" -ForegroundColor Cyan
if (![string]::IsNullOrEmpty($currentValue)) {
$maskedValue = Get-SanitizedValue -VariableName $varName -Value $currentValue
Write-Host "Current value: $maskedValue" -ForegroundColor White
} else {
Write-Host "Current value: (not set)" -ForegroundColor Gray
}
# Get user action for this variable
if (![string]::IsNullOrEmpty($currentValue)) {
# Variable has current value
$action = Read-Host "Action for $varName - (k)eep current, (u)pdate value, or (r)emove? [k/u/r]"
switch ($action.ToLower()) {
'k' {
Write-Host "Keeping current value for $varName" -ForegroundColor Green
}
'u' {
if ($varName.Contains("TOKEN") -or $varName.Contains("KEY") -or $varName.Contains("SECRET")) {
$newValue = Read-Host "Enter new value for $varName" -AsSecureString
$newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue))
} else {
$newValue = Read-Host "Enter new value for $varName"
}
if (![string]::IsNullOrWhiteSpace($newValue)) {
$envVars[$varName].Value = $newValue
Write-Host "Updated $varName" -ForegroundColor Green
} else {
Write-Host "Empty value provided, keeping current value" -ForegroundColor Yellow
}
}
'r' {
Write-Host "Removing $varName" -ForegroundColor Red
$envVars.Remove($varName)
continue
}
default {
Write-Host "Invalid choice, keeping current value for $varName" -ForegroundColor Yellow
}
}
} else {
# Variable has no current value
if ($varName.Contains("TOKEN") -or $varName.Contains("KEY") -or $varName.Contains("SECRET")) {
$newValue = Read-Host "Enter value for $varName (press Enter to remove from WSLENV)" -AsSecureString
$newValue = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($newValue))
} else {
$newValue = Read-Host "Enter value for $varName (press Enter to remove from WSLENV)"
}
if ([string]::IsNullOrWhiteSpace($newValue)) {
Write-Host "Removing $varName (no value provided)" -ForegroundColor Red
$envVars.Remove($varName)
continue
} else {
$envVars[$varName].Value = $newValue
Write-Host "Set value for $varName" -ForegroundColor Green
}
}
# Get flag (only if variable is being kept)
if ($envVars.ContainsKey($varName)) {
$newFlag = Read-Host "Enter WSLENV flag for $varName (press Enter to keep current: $flagText)"
if (![string]::IsNullOrWhiteSpace($newFlag)) {
$envVars[$varName].Flag = $newFlag.TrimStart('/')
}
}
}
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ STEP 2: Add Additional Environment Variables ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
Write-Host "Enter additional environment variables (press Enter with empty key to finish):" -ForegroundColor Green
Write-Host ""
# Interactive loop for additional variables
while ($true) {
$key = Read-Host "Environment variable name"
# Exit condition
if ([string]::IsNullOrWhiteSpace($key)) {
break
}
# Validate key name
if ($key -notmatch '^[A-Za-z_][A-Za-z0-9_]*$') {
Write-Host "Invalid variable name. Use letters, numbers, and underscores only. Must start with letter or underscore." -ForegroundColor Red
continue
}
# Check for duplicate
if ($envVars.ContainsKey($key.ToUpper())) {
Write-Host "Variable '$key' already exists. Skipping." -ForegroundColor Yellow
continue
}
# Get value
do {
if ($key.ToUpper().Contains("TOKEN") -or $key.ToUpper().Contains("KEY") -or $key.ToUpper().Contains("SECRET")) {
$value = Read-Host "Enter value for $key (will be hidden)" -AsSecureString
$value = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($value))
} else {
$value = Read-Host "Enter value for $key"
}
if ([string]::IsNullOrWhiteSpace($value)) {
Write-Host "Value cannot be empty. Please try again." -ForegroundColor Red
}
} while ([string]::IsNullOrWhiteSpace($value))
# Get flag
$flag = Read-Host "Enter WSLENV flag for $key (press Enter for none)"
# Store the variable
$envVars[$key.ToUpper()] = @{
Value = $value
Flag = $flag.TrimStart('/')
}
Write-Host "Added: $($key.ToUpper())" -ForegroundColor Green
Write-Host ""
}
}
# Summary
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ STEP 3: Review Variables to Set ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
foreach ($var in $envVars.GetEnumerator()) {
$flagText = if ([string]::IsNullOrEmpty($var.Value.Flag)) { "(no flag)" } else { "/$($var.Value.Flag)" }
$maskedValue = Get-SanitizedValue -VariableName $var.Key -Value $var.Value.Value
Write-Host " $($var.Key): $maskedValue $flagText" -ForegroundColor White
}
Write-Host ""
$confirm = Read-Host "Proceed with setting these variables? (y/N)"
if ($confirm -ne 'y' -and $confirm -ne 'Y') {
Write-Host "Cancelled." -ForegroundColor Yellow
exit
}
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ STEP 4: Setting Environment Variables ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# Set individual environment variables
$successCount = 0
foreach ($var in $envVars.GetEnumerator()) {
try {
[Environment]::SetEnvironmentVariable($var.Key, $var.Value.Value, "User")
Write-Host "✓ Set $($var.Key)" -ForegroundColor Green
$successCount++
}
catch {
Write-Host "✗ Failed to set $($var.Key): $($_.Exception.Message)" -ForegroundColor Red
}
}
if ($successCount -eq 0) {
Write-Host "No variables were set. Exiting." -ForegroundColor Red
exit 1
}
# Build WSLENV string
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host "║ STEP 5: Configure WSLENV ║" -ForegroundColor Cyan
Write-Host "╚══════════════════════════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# Get current WSLENV to preserve unrelated variables
$currentWSLENV = [Environment]::GetEnvironmentVariable("WSLENV", "User")
$existingParts = @()
if (![string]::IsNullOrEmpty($currentWSLENV)) {
# Find variables that exist in current WSLENV but are NOT in our envVars list
# These will be preserved in the final WSLENV
$existingParts = $currentWSLENV -split ":" | Where-Object {
if (![string]::IsNullOrEmpty($_)) {
$varName = ($_ -split "/" | Select-Object -First 1)
$varName -notin $envVars.Keys
}
}
}
# Build new parts for our variables
$newParts = @()
foreach ($var in $envVars.GetEnumerator()) {
if ([string]::IsNullOrEmpty($var.Value.Flag)) {
$newParts += $var.Key
} else {
$newParts += "$($var.Key)/$($var.Value.Flag)"
}
}
# Combine existing (filtered) + new parts
$allParts = $existingParts + $newParts | Where-Object { ![string]::IsNullOrEmpty($_) }
$wslenvValue = $allParts -join ":"
try {
[Environment]::SetEnvironmentVariable("WSLENV", $wslenvValue, "User")
Write-Host "✓ Set WSLENV: $wslenvValue" -ForegroundColor Green
}
catch {
Write-Host "✗ Failed to set WSLENV: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "╔══════════════════════════════════════════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host "║ SETUP COMPLETE! ║" -ForegroundColor Green
Write-Host "╚══════════════════════════════════════════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host "Environment variables have been set at the User level and configured for WSL access."
Write-Host ""
Write-Host "IMPORTANT: Restart Required" -ForegroundColor Red
Write-Host "=========================" -ForegroundColor Red
Write-Host "1. Close this PowerShell window"
Write-Host "2. Open a new PowerShell or Terminal window"
Write-Host "3. Start a new WSL session"
Write-Host ""
Write-Host "Why restart is needed:" -ForegroundColor Yellow
Write-Host "- Windows needs to reload User environment variables"
Write-Host "- WSL needs to read the updated WSLENV configuration"
Write-Host "- Existing sessions won't see the new variables"
Write-Host ""
Write-Host "To verify environment variables are working:" -ForegroundColor Green
Write-Host ""
Write-Host "Option 1 - From PowerShell (after restart):" -ForegroundColor Cyan
Write-Host " wsl -- printenv | Select-String '$($envVars.Keys -join '|')'"
Write-Host ""
Write-Host "Option 2 - Inside WSL distro (after restart):" -ForegroundColor Cyan
Write-Host " # Start WSL session first:"
Write-Host " wsl"
Write-Host " # Then check variables:"
Write-Host " printenv | grep -E '$($envVars.Keys -join '|')'"
Write-Host " # Or check individual variables:"
foreach ($var in $envVars.Keys) {
Write-Host " echo `$`{$var`}"
}
Write-Host ""
Write-Host "Expected output:" -ForegroundColor Yellow
Write-Host "Each variable should show its name and value (values will be visible in WSL)"

TODO

Environment Variables

COMPLETED

  1. Configuration File Support - Added JSON and .env file support with batch processing mode
  2. Existing Variable Detection - Pre-seeds from existing WSLENV, shows current values with safe removal confirmation
  3. Secure Value Sanitization - Central function showing first/last 4 chars for sensitive values
  4. Interactive vs File Modes - Two distinct operation modes with clear visual workflow
  5. Comprehensive Documentation - README-EnvVars.md with usage examples and troubleshooting

🔄 IN PROGRESS / REMAINING

1. Fix PowerShell Syntax Errors (HIGH PRIORITY)

# Current script has parsing errors preventing execution
# Need to resolve missing closing braces and conditional blocks
# Interactive mode sections need proper structure

2. Variable Validation

# Validate token formats before setting
if ($key -eq "GITHUB_TOKEN" -and $value -notmatch "^ghp_[A-Za-z0-9]{36}$") {
    Write-Warning "GITHUB_TOKEN format looks incorrect"
}
# Add validation for:
# - ANTHROPIC_API_KEY: ^sk-ant-api03-[A-Za-z0-9]{95}$
# - OPENAI_API_KEY: ^sk-[A-Za-z0-9]{48}$
# - AWS keys, etc.

3. Backup & Restore Functionality

# Before making changes, backup current WSLENV
$timestamp = Get-Date -Format "yyyy-MM-dd_HH-mm-ss"
$backup = [Environment]::GetEnvironmentVariable("WSLENV", "User")
Set-Content "wslenv_backup_$timestamp.txt" $backup
# Add -Restore parameter to rollback changes

4. Environment Detection

# Different configs for different environments
# -Environment "Development" | "Production" | "Staging"
# Each could have different variable sets
# Support environment-specific config files: dev.env, prod.env

5. WSL Distribution Targeting

# Test against specific WSL distros
# -TestDistro "Ubuntu-22.04"
# Verify variables work in target distribution
# Post-setup verification in specified distro

6. Secure Storage Integration

# Option to store in Windows Credential Manager instead
# More secure than plain environment variables
# -UseCredentialManager flag

7. Template/Preset Support

# Common presets like "AI-Developer", "GitHub-Developer"
# Pre-configured variable sets for different workflows
# Built-in templates: ai-dev.json, web-dev.json, devops.json

8. Verification Enhancement

# Automatic post-setup verification
# Test actual API calls (optional) to verify tokens work
# -VerifyTokens flag to test GitHub/Anthropic/OpenAI APIs

9. Logging & Audit Trail

# Log what variables were changed (not values)
# Useful for troubleshooting and compliance
# Create change log: envvar_changes_YYYY-MM-DD.log

10. Enhanced File Format Support

# Support YAML configuration files
# Support encrypted configuration files
# Support loading from URLs (secure endpoints)

Priority Order (Updated):

  1. Fix PowerShell Syntax Errors - Script must run without errors
  2. Variable validation - Catches common token format errors
  3. Backup functionality - Allows safe rollback
  4. Environment detection - Multi-environment support
  5. WSL distribution targeting - Cross-distro verification

Recent Improvements Made:

  • ✅ Centralized sanitization function with first/last char display
  • ✅ Two-mode operation (interactive vs file-based)
  • ✅ JSON and .env file format support
  • ✅ Visual workflow with clear step separation
  • ✅ Pre-seeding logic that preserves existing WSLENV
  • ✅ Secure input handling for sensitive values
  • ✅ Comprehensive documentation and examples
  • ✅ Example configuration files (.env.example, secrets.json.example)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment