Stop copy-pasting terminal output into your AI. Let your LLM SSH in and look around.
ShellGuard is an MCP server that gives LLM agents read-only bash access to remote servers over SSH. Connect your AI to production, staging, or dev servers and let it run diagnostics, inspect logs, query databases, and troubleshoot -- hands-free.
Commands are restricted to a curated set of read-only tools. Destructive operations are blocked with actionable suggestions so the LLM can self-correct and keep investigating:
wget -r->"Recursive downloading is not allowed"tail -f->"Follow mode hangs until timeout. Use tail -n 100 for recent lines."sed->"Stream editing can modify files -- read-only access only. Use grep for searching."$HOME/file->"Variable expansion will not expand. Use absolute paths."
brew install jonchun/tap/shellguardOr download the latest binary:
curl -fsSL https://raw.githubusercontent.com/jonchun/shellguard/main/install.sh | shOr with Go:
go install github.com/jonchun/shellguard/cmd/shellguard@latestShellGuard starts as a stdio MCP server -- no arguments needed. Add it to your MCP client of choice:
Cursor
Go to: Settings -> Cursor Settings -> MCP -> Add new global MCP server
Or paste this into your ~/.cursor/mcp.json file. You can also install per-project by creating .cursor/mcp.json in your project folder. See Cursor MCP docs for more info.
{
"mcpServers": {
"shellguard": {
"command": "shellguard"
}
}
}Claude Desktop
Add the following to your Claude Desktop config file. See Claude Desktop MCP docs for more info.
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"shellguard": {
"command": "shellguard"
}
}
}Claude Code
Run this command. See Claude Code MCP docs for more info.
claude mcp add shellguard -- shellguardOpenCode
Add this to your OpenCode configuration file. See OpenCode MCP docs for more info.
{
"mcp": {
"shellguard": {
"type": "local",
"command": ["shellguard"],
"enabled": true
}
}
}VS Code / GitHub Copilot
Add the following to your VS Code settings.json or .vscode/mcp.json. See VS Code MCP docs for more info.
{
"mcp": {
"servers": {
"shellguard": {
"type": "stdio",
"command": "shellguard"
}
}
}
}{
"servers": {
"shellguard": {
"type": "stdio",
"command": "shellguard"
}
}
}Zed
Add the following to your Zed settings file (~/.config/zed/settings.json). See Zed MCP docs for more info.
{
"context_servers": {
"shellguard": {
"command": {
"path": "shellguard",
"args": []
}
}
}
}Roo Code
Go to: Roo Code Settings -> MCP Servers -> Edit MCP Settings
Or add the following to your Roo Code MCP settings file. See Roo Code MCP docs for more info.
{
"mcpServers": {
"shellguard": {
"command": "shellguard"
}
}
}ShellGuard exposes 7 tools to the LLM:
| Tool | Description |
|---|---|
connect |
Establish an SSH connection to a remote host |
execute |
Run a read-only shell command on the remote host |
list_commands |
List available commands, optionally filtered by category |
disconnect |
Close SSH connection(s) |
provision |
Deploy diagnostic tools (rg, jq, yq) to the remote host |
download_file |
Download a file from the remote host via SFTP (50MB limit) |
sleep |
Wait between diagnostic checks (max 15s) |
The LLM connects to a server, runs commands, and reads the output -- the same workflow you'd do manually, but without the context-switching.
Every command goes through a pipeline before reaching the remote host:
- Parse -- bash is parsed into an AST. Shell tricks (semicolons, redirections, command substitution, etc.) are rejected at the syntax level.
- Validate -- commands, flags, and arguments are checked against a curated allowlist of commands (with an explicit denylist). Default-deny.
- Reconstruct -- arguments are re-quoted to prevent injection.
- Execute -- the command runs over SSH with per-command timeouts and output truncation.
For full details, see ARCHITECTURE.md.
ShellGuard tries authentication methods in this order, stopping at the first success:
| Priority | Method | Source | On failure |
|---|---|---|---|
| 1 | Explicit key | identity_file parameter in connect |
Fatal -- connection fails immediately |
| 2 | ssh-agent | SSH_AUTH_SOCK unix socket |
Silent -- skipped |
| 3 | Default keys | ~/.ssh/id_ed25519, id_ecdsa, id_rsa |
Silent -- skipped |
Passphrase-protected keys are silently skipped during default key discovery. If you specify a passphrase-protected key via identity_file, the connection will fail. Add the key to your agent first: ssh-add ~/.ssh/my_key.
ShellGuard supports two SSH modes:
| Mode | Description |
|---|---|
native |
(Default) Uses Go's built-in SSH library. Reads ~/.ssh/config for HostName, User, Port, and IdentityFile. Lightweight, no external dependencies. |
system |
Uses the local ssh binary. Full ~/.ssh/config support including ProxyJump, ProxyCommand, Match blocks, and all other OpenSSH features. Requires ssh to be installed. |
If you connect through bastion hosts, use ProxyJump, or rely on Match blocks in your SSH config, enable system mode:
ssh:
mode: systemexport SHELLGUARD_SSH_MODE=systemIf mode is set to system but the ssh binary is not found, ShellGuard logs a warning and falls back to native mode.
Native mode limitations: Match directives, ProxyJump, ProxyCommand, and ForwardAgent are not supported. Use system mode if your infrastructure requires these features.
System mode notes:
- Host key verification is handled entirely by OpenSSH. The
host_key_checkingandknown_hosts_filesettings only apply in native mode. - Connections are multiplexed using OpenSSH
ControlMaster, so only the first connection per host pays the SSH handshake cost.
ShellGuard verifies SSH host keys using ~/.ssh/known_hosts. Three modes are available:
| Mode | Behavior |
|---|---|
accept-new |
(Default) Trust-on-first-use. Unknown hosts are accepted and written to known_hosts. Key changes are rejected. |
strict |
Require the host key to already exist in known_hosts. Unknown hosts are rejected. |
off |
Disable host key verification entirely. |
If a host key has changed, remove the old entry from your known_hosts file.
Settings can be specified in a YAML config file or via environment variables. Environment variables take precedence.
Config file location: $XDG_CONFIG_HOME/shellguard/config.yaml (default: ~/.config/shellguard/config.yaml)
ssh:
mode: "native" # native | system
connect_timeout: "10s" # default 10s
retries: 2 # default 2
retry_backoff: "250ms" # default 250ms
host_key_checking: "accept-new" # accept-new | strict | off (native mode only)
known_hosts_file: "~/.ssh/known_hosts" # native mode only| YAML field | Environment variable | Default | Description |
|---|---|---|---|
ssh.mode |
SHELLGUARD_SSH_MODE |
native |
SSH mode: native (built-in) or system (uses local ssh binary) |
ssh.connect_timeout |
SHELLGUARD_SSH_CONNECT_TIMEOUT |
10s |
TCP + SSH handshake timeout |
ssh.retries |
SHELLGUARD_SSH_RETRIES |
2 |
Connection/execution retry attempts |
ssh.retry_backoff |
SHELLGUARD_SSH_RETRY_BACKOFF |
250ms |
Base backoff (exponential: backoff * 2^attempt) |
ssh.host_key_checking |
SHELLGUARD_SSH_HOST_KEY_CHECKING |
accept-new |
Host key verification mode (native mode only) |
ssh.known_hosts_file |
SHELLGUARD_SSH_KNOWN_HOSTS_FILE |
~/.ssh/known_hosts |
Path to known_hosts file (native mode only) |
Remote servers don't always have the tools you want. On connect, ShellGuard probes for rg, jq, and yq. If any are missing, the LLM can call provision to deploy them:
| Tool | Version | Architectures |
|---|---|---|
rg (ripgrep) |
14.1.1 | x86_64, aarch64 |
jq |
1.7.1 | x86_64, aarch64 |
yq |
4.52.2 | x86_64, aarch64 |
Binaries are downloaded from GitHub Releases with SHA-256 verification, cached locally, and deployed to ~/.shellguard/bin/ on the remote host.
ShellGuard can be used as a Go library:
package main
import (
"context"
"log/slog"
"os"
"github.com/jonchun/shellguard"
)
func main() {
ctx := context.Background()
logger := slog.New(slog.NewTextHandler(os.Stderr, nil))
err := shellguard.RunStdio(ctx, shellguard.Config{Logger: logger})
if err != nil {
os.Exit(1)
}
}See the Custom Configuration and Custom Executor sections below for advanced usage.
import (
"github.com/jonchun/shellguard"
"github.com/jonchun/shellguard/manifest"
"github.com/jonchun/shellguard/server"
)
manifests, _ := manifest.LoadEmbedded()
// Add or remove commands as needed
core, err := shellguard.New(shellguard.Config{
Manifests: manifests, // Custom registry (nil = embedded defaults)
Executor: myCustomExecutor, // Custom backend (nil = SSH)
Name: "my-server", // MCP server name
Version: "1.0.0", // MCP server version
})Implement the server.Executor interface to use non-SSH backends:
type Executor interface {
Connect(ctx context.Context, params ssh.ConnectionParams) error
Execute(ctx context.Context, host, command string, timeout time.Duration) (ssh.ExecResult, error)
ExecuteRaw(ctx context.Context, host, command string, timeout time.Duration) (ssh.ExecResult, error)
SFTPSession(host string) (ssh.SFTPClient, error)
Disconnect(host string) error
}make test # Run all tests
make test-race # Run with race detector
make lint # Run go vetshellguard/
shellguard.go # Top-level constructor (New, RunStdio)
cmd/shellguard/ # CLI entrypoint
server/ # MCP server core, tool registration, Executor interface
parser/ # Shell AST parser (mvdan.cc/sh/v3)
validator/ # Command/flag/SQL validation engine
manifest/ # YAML command registry (embed.FS)
manifests/ # allowed command manifests
manifests/denied/ # denied command manifests
ssh/ # SSH manager, ShellQuote, ReconstructCommand
output/ # Output truncation (64KB cap)
toolkit/ # Diagnostic tool provisioning (rg, jq, yq)
Apache License 2.0. See LICENSE for details.