Configuration
Configuration Files
SSHIFT uses a priority-based configuration system with multiple config file locations.
Environment Variables (.env files)
Environment variables are loaded from multiple locations. Since dotenv does not overwrite existing variables, the first file to set a variable wins:
| Priority | Path | Notes |
|---|---|---|
| 1 | <PACKAGE_DIR>/.env/.env.local |
Package directory (local override) |
| 2 | <PACKAGE_DIR>/.env.local |
Package directory (local) |
| 3 | <PACKAGE_DIR>/.env/.env |
Package directory (shared) |
| 4 | <PACKAGE_DIR>/.env |
Package directory (base) |
| 5 | ~/.local/share/sshift/.env/.env.local |
User install (local override) |
| 6 | ~/.local/share/sshift/.env.local |
User install (local) |
| 7 | ~/.local/share/sshift/.env/.env |
User install (shared) |
| 8 | ~/.local/share/sshift/.env |
User install (base) |
The CLI entry point (sshift) additionally loads .env files from its own script directory before the server’s env-loader runs.
Example .env/.env.local:
# SSH Test Credentials
SSH_HOST=192.168.1.100
SSH_PORT=22
SSH_USER=myuser
SSH_PASS=mypassword
# Or use TEST_* variables
TEST_HOST=192.168.1.100
TEST_PORT=22
TEST_USER=testuser
TEST_PASS=testpassword
Configuration File (config.json)
The application configuration (bookmarks, settings) is searched in the following locations. The first match wins; remaining paths are ignored.
| Priority | Path | Notes |
|---|---|---|
| 1 | <PACKAGE_DIR>/.env/config.json |
NPM package directory |
| 2 | <PACKAGE_DIR>/config.json |
NPM package root (created by ensureConfig() if no config found) |
| 3 | ~/.local/share/sshift/.env/config.json |
User install location |
| 4 | ~/.local/share/sshift/config.json |
User install (no .env subdir) |
If no config file exists at any path, ensureConfig() creates one at <PACKAGE_DIR>/config.json.
Example .env/config.json:
{
"port": 8022,
"devPort": 3000,
"bind": "0.0.0.0",
"enableHttps": true,
"sticky": true,
"sshKeepaliveInterval": 15000,
"sshKeepaliveCountMax": 500,
"bookmarks": [
{
"id": "1701234567890",
"name": "Production Server",
"host": "prod.example.com",
"port": 22,
"username": "deploy",
"type": "ssh"
},
{
"id": "1701234567891",
"name": "Development Server",
"host": "dev.example.com",
"port": 22,
"username": "developer",
"type": "ssh"
}
],
"settings": {
"fontSize": 14,
"fontFamily": "'Courier New', monospace",
"theme": "dark"
}
}
Configuration Options
Server Settings
port(number): Server port (default:8022)devPort(number): Development server port (default:3000)bind(string): Bind address (default:"0.0.0.0")enableHttps(boolean): Enable HTTPS with self-signed certificates (default:true)-
certPath(stringnull): Absolute path to a custom TLS certificate file (PEM format). Both certPathandkeyPathmust be set together (default:null) -
keyPath(stringnull): Absolute path to a custom TLS private key file (PEM format). Both certPathandkeyPathmust be set together (default:null) sticky(boolean): Enable sticky sessions (default:true)
SSH Settings
sshKeepaliveInterval(number): SSH keepalive interval in milliseconds (default:15000)sshKeepaliveCountMax(number): Maximum keepalive count (default:500)
Password Protection
-
passwordHash(stringnull): SHA-256 hash of a password to restrict access to the application. When set, all API endpoints and WebSocket connections require authentication. Set to null(default) to disable password protection.
Note: Password protection is intended as a basic access restriction for local/private networks. It is not a replacement for proper authentication. If you expose sshift to a public network, use additional security measures such as a reverse proxy with authentication, a VPN, or firewall rules.
Password protection can also be enabled/disabled through the Settings UI in the application. When enabling, you will be prompted to set a password; when disabling, you must provide the current password.
HTTPS Configuration
By default, sshift uses HTTPS with self-signed certificates. This provides:
- Secure WebSocket connections (WSS)
- Better mobile device support for text selection
- Encrypted communication
When HTTPS is enabled, sshift automatically generates a self-signed certificate valid for:
localhost- Your machine’s hostname
- All local IP addresses
Note: Your browser will show a security warning for self-signed certificates. This is normal for development/local use. Click “Advanced” → “Proceed to localhost (unsafe)” to continue.
Custom Certificate Paths
You can specify your own trusted certificates in config.json using certPath and keyPath:
{
"enableHttps": true,
"certPath": "/path/to/your/certificate.pem",
"keyPath": "/path/to/your/private-key.pem"
}
Both certPath and keyPath must be set together; if only one is provided, sshift will fall back to its self-signed certificate. Use absolute paths for reliability.
HTTPS on Local Network (LAN) — PWA and “Not Secure” Warnings
When accessing sshift from a device on your local network (e.g., https://192.168.1.50:8022), browsers will display a “Not Secure” warning because the self-signed certificate is not trusted. This also prevents Progressive Web App (PWA) installation, which requires a trusted secure context.
There are several ways to resolve this:
Option 1: Chrome “Insecure Origins Treated as Secure” Flag (Quick)
This is the fastest method for development or personal use. It tells Chrome to treat a specific origin as secure, enabling PWA features.
- Open Chrome and navigate to:
chrome://flags/#unsafely-treat-insecure-origin-as-secure - In the text box, enter your sshift LAN URL, e.g.:
https://192.168.1.50:8022Include the protocol (
https://) and port number. - Set the dropdown to Enabled.
- Click the Relaunch button at the bottom to restart Chrome.
After relaunching, Chrome will treat that origin as a secure context — the “Not Secure” warning will disappear, and you can install sshift as a PWA.
Note: This flag is per-device and per-browser. Each device on your LAN needs its own configuration. It is intended for development and personal use, not production.
Option 2: Custom Trusted Certificate
For a more permanent solution, create a certificate for your LAN IP and add it to your device’s trusted root store.
Step 1: Generate a certificate for your LAN IP
Using OpenSSL:
# Create a config file for the certificate
cat > sshift-lan.cnf <<EOF
[req]
default_bits = 2048
prompt = no
distinguished_name = dn
x509_extensions = v3_req
[dn]
CN = sshift
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = your-hostname
IP.1 = 192.168.1.50
IP.2 = 127.0.0.1
EOF
# Generate the certificate and private key
openssl req -new -x509 -days 3650 -nodes \
-keyout sshift-lan-key.pem \
-out sshift-lan-cert.pem \
-config sshift-lan.cnf
Replace 192.168.1.50 with your actual LAN IP and your-hostname with your machine’s hostname.
Step 2: Configure sshift to use the certificate
Add the certificate paths to your config.json:
{
"enableHttps": true,
"certPath": "/path/to/sshift-lan-cert.pem",
"keyPath": "/path/to/sshift-lan-key.pem"
}
Step 3: Trust the certificate on your devices
- Windows: Double-click the
.pemfile → Install Certificate → Local Machine → Place all certificates in “Trusted Root Certification Authorities” - macOS: Double-click the
.pemfile → Add to Keychain → Set to “Always Trust” in Keychain Access - Linux: Copy to
/usr/local/share/ca-certificates/and runsudo update-ca-certificates - Android: Settings → Security → Install from storage → Select the
.pemfile - iOS: Send the file via AirDrop/email → Open → Install profile → Go to Settings → General → About → Certificate Trust Settings → Enable full trust
After trusting the certificate, the “Not Secure” warning will disappear and PWA installation will work.
Option 3: Reverse Proxy with nginx
For production or multi-device deployments, use nginx as a reverse proxy with a trusted certificate (e.g., from Let’s Encrypt or a self-signed CA).
Example nginx configuration:
server {
listen 443 ssl;
server_name sshift.lan;
ssl_certificate /etc/nginx/ssl/sshift-cert.pem;
ssl_certificate_key /etc/nginx/ssl/sshift-key.pem;
location / {
proxy_pass https://127.0.0.1:8022;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_verify off;
}
}
Then configure sshift to listen on localhost only:
{
"bind": "127.0.0.1",
"port": 8022,
"enableHttps": true
}
Note: If you use nginx with HTTPS in front, you can also set
"enableHttps": falsein sshift’s config to have nginx handle all TLS termination. Configure nginx to proxy tohttp://127.0.0.1:8022in that case.
Option 4: Local DNS with mDNS/Avahi
Assign a .local hostname to your machine using mDNS, then use that hostname in your browser. Combined with Option 2 or 3, this provides a clean URL like https://sshift.local instead of an IP address.
# Install avahi (Linux)
sudo apt install avahi-daemon
# Verify your .local hostname
avahi-resolve -4 --name your-hostname.local
Comparison of HTTPS/LAN Options
| Method | Ease | Per-Device Setup | PWA Support | Trust Level |
|---|---|---|---|---|
| Chrome flag | Easiest | Yes (each browser) | Yes | Dev/personal only |
| Trusted cert | Moderate | Yes (each OS) | Yes | Full |
| nginx reverse proxy | Advanced | No (trust once) | Yes | Full |
| mDNS hostname | Moderate | No | Yes (with cert) | Full |
Custom Layouts
SSHIFT supports custom terminal layouts that can be defined in config.json. Layouts allow you to split your terminal into multiple panels for multitasking.
Layout Structure
Each layout consists of:
id- Unique identifiername- Display name shown in the UIicon- Lucide icon name (e.g., “square”, “columns-2”, “grid-2x2”)columns- Array of column definitions
Each column has:
width- Column width (percentage string, e.g., “50%”, “33.33%”)rows- Array of row definitions within the column
Each row has:
height- Row height (percentage string, e.g., “100%”, “50%”)
Example Custom Layouts
{
"layouts": [
{
"id": "single",
"name": "Single",
"icon": "square",
"columns": [
{
"width": "100%",
"rows": [{ "height": "100%" }]
}
]
},
{
"id": "horizontal-split",
"name": "Horizontal Split",
"icon": "columns-2",
"columns": [
{
"width": "50%",
"rows": [{ "height": "100%" }]
},
{
"width": "50%",
"rows": [{ "height": "100%" }]
}
]
},
{
"id": "vertical-split",
"name": "Vertical Split",
"icon": "rows-2",
"columns": [
{
"width": "100%",
"rows": [
{ "height": "50%" },
{ "height": "50%" }
]
}
]
},
{
"id": "grid-2x2",
"name": "Grid 2x2",
"icon": "grid-2x2",
"columns": [
{
"width": "50%",
"rows": [
{ "height": "50%" },
{ "height": "50%" }
]
},
{
"width": "50%",
"rows": [
{ "height": "50%" },
{ "height": "50%" }
]
}
]
}
]
}
Configuration Priority
When the same setting is defined in multiple places, SSHIFT uses this priority (highest to lowest):
Port Priority
--portCLI argument (highest priority; setsPORTenv var)PORTenvironment variable (from.envfiles or shell)config.jsondevPort(whenNODE_ENV=developmentor--dev)config.jsonport(production)- Built-in defaults — 8022 (production), 3000 (development)
Bind Address Priority
--bindCLI argument (highest priority; setsBINDenv var)BINDenvironment variable (from.envfiles or shell)config.jsonbindsetting- Built-in default —
"0.0.0.0"
.env File Priority (first setter wins)
See the Environment Variables table above. Since dotenv does not overwrite existing variables, the first .env file to set a variable takes precedence.
Config File Priority (first match wins)
See the Configuration File table above. The first config.json found in the search path is used.
Security Considerations
Sensitive Data
Never commit sensitive data to version control!
- Use
.env/.env.localfor passwords and credentials - Add
.env/to your.gitignorefile - Use
config.json.exampleas a template (without real credentials)
File Permissions
# Set appropriate permissions for config files
chmod 600 .env/.env.local
chmod 600 .env/config.json
Example .gitignore
# Environment files
.env/
.env.local
# Config files with sensitive data
config.json
# Keep example config
!config.json.example
Plugins
SSHIFT supports a plugin system that can observe SSH session data and terminal output, and react to events like tab flashing. Plugins are configured in config.json under the plugins array.
Built-in Plugins
OpenCode Attention (opencode-attention)
Detects when OpenCode is waiting for user input and flashes the browser tab. Tracks OpenCode’s spinner characters (⬝ ■ ▣) and prompt patterns. When the spinner stops or a prompt is detected, the tab flashes until you focus it.
Configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
debounceMs |
number | 300 |
Milliseconds between full-state checks |
flashDuration |
number | 0 |
Flash duration in ms. 0 = flash until focused |
checkInterval |
number | 2000 |
Milliseconds between periodic terminal state checks |
idleThreshold |
number | 3000 |
Milliseconds without spinner before considered idle |
patterns |
string[] | — | Additional regex patterns to detect attention |
excludePatterns |
string[] | — | Regex patterns to exclude from detection |
Example:
{
"name": "opencode-attention",
"enabled": true,
"config": {
"debounceMs": 300,
"flashDuration": 0,
"idleThreshold": 3000
}
}
Claude Attention (claude-attention)
Detects when Claude Code is waiting for user input and flashes the browser tab. Tracks Claude’s spinner characters (braille spinners ⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⠽⠛ and v2 spinners ·✢✳✶✻✽) and prompt patterns like “❯”, “Do you want”, “Allow”, and “Esc to cancel”. Only activates detection once a Claude session is confirmed. While Claude is actively working (spinner active), all flashing is suppressed to avoid false positives.
Configuration options:
| Option | Type | Default | Description |
|---|---|---|---|
debounceMs |
number | 300 |
Milliseconds between full-state checks |
flashDuration |
number | 0 |
Flash duration in ms. 0 = flash until focused |
checkInterval |
number | 2000 |
Milliseconds between periodic terminal state checks |
idleThreshold |
number | 3000 |
Milliseconds without spinner before considered idle |
cooldownMs |
number | 1000 |
Milliseconds to suppress re-flash after spinner stops a flash |
patterns |
string[] | — | Additional regex patterns to detect attention |
excludePatterns |
string[] | — | Regex patterns to exclude from detection |
Example:
{
"name": "claude-attention",
"enabled": true,
"config": {
"debounceMs": 300,
"flashDuration": 0,
"idleThreshold": 3000,
"cooldownMs": 1000
}
}
Full Plugin Configuration Example
{
"plugins": [
{
"name": "opencode-attention",
"enabled": true,
"config": {
"debounceMs": 300,
"flashDuration": 0
}
},
{
"name": "claude-attention",
"enabled": true,
"config": {
"debounceMs": 300,
"flashDuration": 0,
"idleThreshold": 3000,
"cooldownMs": 1000
}
}
]
}
Disabling a Plugin
Set "enabled": false to disable a plugin without removing its configuration:
{
"name": "opencode-attention",
"enabled": false,
"config": { }
}