Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,7 @@ npm/cli-*/bin/
# Local release artifacts
*.tar.gz
*.zip

# Agent instructions
AGENTS.md
CLAUDE.md
2 changes: 1 addition & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ brews:
name: homebrew-tap
token: "{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}"
homepage: "https://shellroute.com"
description: "Residential proxy CLI"
description: "A proxied shell for terminal workflows. Open a session, choose a proxy, and run commands normally."
directory: Formula

signs:
Expand Down
35 changes: 26 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)

Run terminal commands through country-specific residential or datacenter proxies. No VPN, no per-tool configuration. Learn more at [shellroute.com](https://shellroute.com/).
**Every terminal can be somewhere else.** Open a proxied shell or route one command, then run your terminal workflow normally. Learn more at [shellroute.com](https://shellroute.com/).

## Install

Expand All @@ -29,16 +29,22 @@ Supports macOS and Linux.
## Quick start

```bash
# Log in (creates account if new)
# Log in. A new account is created automatically.
shellroute login

# Start a proxy
shellroute proxy --country US
# Shellroute opens after login. Run these inside it:
/connect US
curl https://ipinfo.io/json
```

# In another terminal
curl -x http://127.0.0.1:41900 https://ipinfo.io/ip
### Or route one command

```bash
shellroute run DE -- curl https://ipinfo.io/json
```

[Read the full quickstart](https://shellroute.com/docs/quickstart?utm_source=github&utm_medium=readme&utm_campaign=cli_readme).

## Commands

### Authentication
Expand All @@ -58,6 +64,17 @@ curl -x http://127.0.0.1:41900 https://ipinfo.io/ip
| `shellroute proxy --country <code>` | Persistent proxy (blocks until Ctrl+C) |
| `shellroute proxy stop` | Stop running proxy sessions |

#### Local proxy mode

For tools configured with an explicit proxy URL:

```bash
shellroute proxy --country US

# In another terminal
curl -x http://127.0.0.1:41900 https://ipinfo.io/ip
```

### Info

| Command | Description |
Expand Down Expand Up @@ -86,13 +103,13 @@ Run all checks (lint, tests, audit, cross-compile): `./scripts/run-tests.sh`. Re
Your terminal -> shellroute CLI (local proxy) -> shellroute API -> Gateway -> Exit IP -> Internet
```

The CLI runs a local HTTP proxy on `127.0.0.1` and sets `HTTP_PROXY`/`HTTPS_PROXY` for the child process. Proxy-aware tools such as curl, Python Requests, and HTTPX inherit the route. Some clients need explicit configuration. See the [compatibility matrix](docs/compatibility.md) for tested versions and conditions.
Shellroute implements each active route as a local HTTP proxy and provides standard proxy environment variables to the shell or child process. Clients that use those variables send requests through the selected proxy. Each session remains independent, while shellroute manages credentials, rotation, usage, and cleanup.

Traffic exits through residential or datacenter IPs in 120+ countries.
[See what shellroute proxies.](docs/compatibility.md)

## Important

The shellroute CLI is open source. It connects to the shellroute service, which requires a paid account. See [shellroute.com](https://shellroute.com/) for pricing and [acceptable use policy](https://shellroute.com/acceptable-use).
The shellroute CLI is open source and connects to the shellroute service. The service uses prepaid credits. See [pricing](https://shellroute.com/pricing) and the [acceptable use policy](https://shellroute.com/acceptable-use).

## Privacy

Expand Down
7 changes: 4 additions & 3 deletions internal/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,10 @@ type LocationsResponse struct {
}

type LoginResponse struct {
APIKey string `json:"api_key,omitempty"` // empty — key is generated locally
Email string `json:"email"`
BalanceUSD float64 `json:"balance_usd"`
APIKey string `json:"api_key,omitempty"` // empty — key is generated locally
Email string `json:"email"`
BalanceUSD float64 `json:"balance_usd"`
StarterCredit string `json:"starter_credit,omitempty"`
}

type VersionResponse struct {
Expand Down
12 changes: 10 additions & 2 deletions internal/cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func runLoginVerifyCode() error {
keyHash := auth.HashClientKey(rawKey)
keyPrefix := auth.KeyPrefix(rawKey)

_, err = client.VerifyLoginCode(loginEmail, loginVerifyCode, keyHash, keyPrefix)
resp, err := client.VerifyLoginCode(loginEmail, loginVerifyCode, keyHash, keyPrefix)
if err != nil {
if apiErr, ok := err.(*api.APIError); ok {
display.Error("%s", apiErr.UserMessage())
Expand All @@ -135,6 +135,10 @@ func runLoginVerifyCode() error {
}

display.Success("Logged in successfully")
if resp != nil && resp.StarterCredit != "" {
fmt.Fprintln(os.Stderr)
display.InfoBold("%s", resp.StarterCredit)
}
return nil
}

Expand Down Expand Up @@ -181,7 +185,7 @@ func runLoginInteractive() error {
keyHash := auth.HashClientKey(rawKey)
keyPrefix := auth.KeyPrefix(rawKey)

_, err = client.VerifyLoginCode(email, code, keyHash, keyPrefix)
resp, err := client.VerifyLoginCode(email, code, keyHash, keyPrefix)
if err != nil {
if apiErr, ok := err.(*api.APIError); ok {
display.Error("%s", apiErr.UserMessage())
Expand All @@ -198,6 +202,10 @@ func runLoginInteractive() error {
}

display.Success("Logged in successfully")
if resp != nil && resp.StarterCredit != "" {
fmt.Fprintln(os.Stderr)
display.InfoBold("%s", resp.StarterCredit)
}

// Enter interactive mode
tui.Version = Version()
Expand Down
4 changes: 4 additions & 0 deletions internal/display/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ func Info(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, " %s\n", fmt.Sprintf(msg, args...))
}

func InfoBold(msg string, args ...interface{}) {
fmt.Fprintf(os.Stderr, " \033[1m%s\033[0m\n", fmt.Sprintf(msg, args...))
}

func Label(label, value string) {
fmt.Fprintf(os.Stderr, " %s %s\n",
labelStyle.Render(label+":"),
Expand Down
10 changes: 2 additions & 8 deletions internal/session/session.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,6 @@ type StartOpts struct {
Mode string // "proxy", "run", or "" (interactive)
}

// StartWithCredentials starts a local proxy using pre-existing session credentials
// (from a rotate response). Does not call the API to create a session.
func StartWithCredentials(ctx context.Context, client *api.Client, resp *api.SessionCreateResponse, port int) (*Session, error) {
return startWithResponse(ctx, client, resp, port, StartOpts{})
}

// Start creates a new session via the API and starts the local proxy.
func Start(ctx context.Context, client *api.Client, req *api.SessionCreateRequest, port int, opts ...StartOpts) (*Session, error) {
// Bind port before API call — fail fast if port unavailable
Expand All @@ -112,9 +106,9 @@ func Start(ctx context.Context, client *api.Client, req *api.SessionCreateReques
return startWithResponse(ctx, client, resp, port, sOpts)
}

// startWithResponse is the shared implementation for Start and StartWithCredentials.
// startWithResponse is the shared implementation for session start.
func startWithResponse(ctx context.Context, client *api.Client, resp *api.SessionCreateResponse, port int, opts StartOpts) (*Session, error) {
// If port not yet bound (StartWithCredentials path), bind now
// If port not yet bound, bind now
if port == 0 {
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
Expand Down
35 changes: 4 additions & 31 deletions internal/tui/tui.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ func Run(cfg *config.Config) error {

client := api.New(cfg.APIURL, cfg.APIKey)

// Show balance warning on startup
// Show balance warning on startup (threshold from server)
if bal, err := client.GetBalance(); err == nil {
if bal.BalanceUSD == 0 {
display.Warn("Balance $0.00 — top up to use at https://console.shellroute.com")
} else if bal.BalanceUSD < 0.50 {
} else if bal.LowBalance {
display.Warn("Balance low (%s) — top up at https://console.shellroute.com", display.FormatBalance(bal.BalanceUSD))
}
}
Expand All @@ -43,35 +43,6 @@ func Run(cfg *config.Config) error {

runShell(ctrlPort, "", cfg.DefaultType)

// Disconnect if session is still active
if resp := ctrl.StopAndDisconnect(); resp != nil {
printSessionSummary(resp)
}

return nil
}

// RunWithConnect starts the shell and auto-connects to a country.
func RunWithConnect(cfg *config.Config, country string) error {
client := api.New(cfg.APIURL, cfg.APIKey)

if bal, err := client.GetBalance(); err == nil {
if bal.BalanceUSD == 0 {
display.Warn("Balance $0.00 — top up to use at https://console.shellroute.com")
} else if bal.BalanceUSD < 0.50 {
display.Warn("Balance low (%s) — top up at https://console.shellroute.com", display.FormatBalance(bal.BalanceUSD))
}
}

ctrl := session.NewController(client, cfg)

ctrlPort, err := ctrl.Start()
if err != nil {
return fmt.Errorf("control server: %w", err)
}

runShell(ctrlPort, country, cfg.DefaultType)

if resp := ctrl.StopAndDisconnect(); resp != nil {
printSessionSummary(resp)
}
Expand Down Expand Up @@ -114,9 +85,11 @@ func runShell(ctrlPort int, autoConnect string, defaultType string) {
if defaultType == "" {
defaultType = "residential"
}
cfgDir, _ := config.Dir()
env := append(os.Environ(),
fmt.Sprintf("SHELLROUTE_CTRL=%d", ctrlPort),
fmt.Sprintf("SHELLROUTE_IPTYPE=%s", defaultType),
fmt.Sprintf("SHELLROUTE_CONFIG_DIR=%s", cfgDir),
"SHELL_SESSIONS_DISABLE=1", // suppress macOS session restore on exit
"BASH_SILENCE_DEPRECATION_WARNING=1", // suppress macOS "default shell is now zsh" nag
)
Expand Down
4 changes: 2 additions & 2 deletions internal/tui/tui_shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,8 +310,8 @@ func writeSettingsCommands(f *os.File) {
}

/logout() {
# Clear stored credentials
local config_file="${HOME}/.shellroute/config.toml"
# Clear stored credentials (respects local mode via SHELLROUTE_CONFIG_DIR)
local config_file="${SHELLROUTE_CONFIG_DIR:-${HOME}/.shellroute}/config.toml"
if [ -f "$config_file" ]; then
sed -i.bak 's/^api_key = .*/api_key = ""/' "$config_file" 2>/dev/null || \
sed -i '' 's/^api_key = .*/api_key = ""/' "$config_file" 2>/dev/null
Expand Down
2 changes: 1 addition & 1 deletion npm/cli-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shellroute/cli-darwin-arm64",
"version": "0.1.0",
"version": "0.0.0",
"os": [
"darwin"
],
Expand Down
2 changes: 1 addition & 1 deletion npm/cli-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shellroute/cli-darwin-x64",
"version": "0.1.0",
"version": "0.0.0",
"os": [
"darwin"
],
Expand Down
2 changes: 1 addition & 1 deletion npm/cli-linux-arm64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shellroute/cli-linux-arm64",
"version": "0.1.0",
"version": "0.0.0",
"os": [
"linux"
],
Expand Down
2 changes: 1 addition & 1 deletion npm/cli-linux-x64/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@shellroute/cli-linux-x64",
"version": "0.1.0",
"version": "0.0.0",
"os": [
"linux"
],
Expand Down
26 changes: 26 additions & 0 deletions npm/shellroute/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# shellroute

A proxied shell for terminal workflows. Every terminal can be somewhere else.

## Install

```bash
npm install -g shellroute
```

## First session

```bash
# Log in. A new account is created automatically.
shellroute login

# Shellroute opens after login. Run these inside it:
/connect US
curl https://ipinfo.io/json
```

[Follow the two-minute quickstart](https://shellroute.com/docs/quickstart?utm_source=npm&utm_medium=package-readme&utm_campaign=cli_readme).

The CLI is open source and connects to the shellroute service. The service uses prepaid credits.

[GitHub](https://github.com/shellroute/shellroute-cli) · [Pricing](https://shellroute.com/pricing) · [Compatibility](https://shellroute.com/docs/compatibility) · [Acceptable use](https://shellroute.com/acceptable-use)
29 changes: 21 additions & 8 deletions npm/shellroute/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "shellroute",
"version": "0.1.0",
"description": "Residential proxy CLI",
"version": "0.0.0",
"description": "A proxied shell for terminal workflows. Open a session, choose a proxy, and run commands normally.",
"license": "Apache-2.0",
"repository": {
"type": "git",
Expand All @@ -11,15 +11,28 @@
"files": [
"bin",
"LICENSE",
"NOTICE"
"NOTICE",
"README.md"
],
"bin": {
"shellroute": "bin/shellroute"
},
"optionalDependencies": {
"@shellroute/cli-darwin-arm64": "0.1.0",
"@shellroute/cli-darwin-x64": "0.1.0",
"@shellroute/cli-linux-arm64": "0.1.0",
"@shellroute/cli-linux-x64": "0.1.0"
}
"@shellroute/cli-darwin-arm64": "0.0.0",
"@shellroute/cli-darwin-x64": "0.0.0",
"@shellroute/cli-linux-arm64": "0.0.0",
"@shellroute/cli-linux-x64": "0.0.0"
},
"keywords": [
"automation",
"cli",
"command-line-tool",
"developer-tools",
"geo-testing",
"http-proxy",
"network-debugging",
"proxy",
"shell",
"terminal"
]
}
Loading