Skip to content

Commit 715aef2

Browse files
committed
feat(install): ask for network binding, default to LAN access with password prompt
The loopback-only default was safe but left most installs unreachable from the devices people actually use. The installer now asks at the end of setup: 1) Any device on your network (0.0.0.0), the default. Prompts for a dashboard password (confirmed twice); skipping it requires an explicit confirmation and prints a big red warning as the final output. 2) This machine only (127.0.0.1), the safer option, for tunnel/Tailscale setups. The choice flows into the systemd unit, the launchd plist (values escaped for both formats), the run-now exec path, and the printed URLs (LAN IP detection included). Non-interactive installs keep the safe loopback default unless CODEMAN_HOST is preset; the server binary's own default binding is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 608ec8a commit 715aef2

2 files changed

Lines changed: 229 additions & 15 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ curl -fsSL https://raw.githubusercontent.com/Ark0N/Codeman/master/install.sh | b
4646
This installs Node.js and tmux if missing, clones Codeman to `~/.codeman/app`, and builds it. A few things worth knowing:
4747

4848
- **It asks first.** Every system change (package installs, AI CLI download) is prompted, and a menu at the end lets you choose: run Codeman in this terminal, install it as a background service (systemd/launchd, auto-start on boot), or don't start yet. Nothing runs in the background unless you pick it.
49+
- **Network or local-only, your choice.** The installer asks whether the dashboard should be reachable from other devices on your network (`0.0.0.0`, the default, with a strongly recommended password prompt) or from this machine only (`127.0.0.1`, safest). Skipping the password on a network bind requires an explicit confirmation and ends with a loud warning. A bare `codeman web` started by hand still defaults to loopback.
4950
- **Re-run to update.** The same one-liner updates a finished install in place: local changes in `~/.codeman/app` are stashed (never discarded), and a running service is restarted and verified. If a first install was interrupted, re-running resumes the full setup instead. `install.sh update` and `install.sh uninstall` also exist.
5051
- **CI / headless:** without a terminal attached, steps that would change your system abort with instructions instead of running silently. Set `CODEMAN_NONINTERACTIVE=1` to approve them for automation.
5152

@@ -590,7 +591,7 @@ By default Codeman launches sessions with `--dangerously-skip-permissions`, so t
590591

591592
### Network & access
592593

593-
- **Loopback by default** — binds `127.0.0.1`, reachable only from the same machine, so the no-password default is safe out of the box. Binding a non-loopback host without `CODEMAN_PASSWORD` _starts but prints a loud warning_ with three concrete fixes (set a password, loopback + an authenticated tunnel, or explicitly acknowledge with `--allow-unauthenticated-network`)
594+
- **Loopback by default**the server binary binds `127.0.0.1`, reachable only from the same machine, so the no-password default is safe out of the box (the guided installer asks about network access and configures the binding + password for you). Binding a non-loopback host without `CODEMAN_PASSWORD` _starts but prints a loud warning_ with three concrete fixes (set a password, loopback + an authenticated tunnel, or explicitly acknowledge with `--allow-unauthenticated-network`)
594595
- **Optional auth, real sessions** — HTTP Basic via `CODEMAN_USERNAME` (default `admin`) / `CODEMAN_PASSWORD`. Success issues an opaque 256-bit `codeman_session` cookie (`randomBytes(32)`) — validated server-side, not client-signed, so it can't be forged offline (24h TTL, auto-extend, device-context audit log)
595596
- **Per-IP rate limiting** — 10 failed attempts → `429` with `Retry-After` (15-min decay). A valid cookie or correct password recovers _immediately_ even while an attacker hammers the same IP — important because all tunnel traffic shares one loopback IP. QR auth has its own separate limiter
596597
- **Configurable permission mode** - `--dangerously-skip-permissions` is only the default. **App Settings → Claude CLI → Startup Mode** can switch new sessions to Anthropic's classifier-guarded `auto` mode (low-prompt, needs Claude Code 2.1.207+), `normal` prompting, or an explicit allowed-tools list. In multi-user mode, non-granted users are forced to `auto`, and shell sessions / skip-permissions require an explicit per-user grant

install.sh

Lines changed: 227 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
# CODEMAN_NODE_VERSION - Node.js major version to install (default: 22)
1616
# CODEMAN_REPO_URL - Custom git repository URL (default: upstream Codeman)
1717
# CODEMAN_BRANCH - Git branch to install (default: master)
18+
# CODEMAN_HOST - Preset the network binding and skip the prompt
19+
# (e.g. 0.0.0.0 for LAN access, 127.0.0.1 for
20+
# local-only; interactive default is 0.0.0.0,
21+
# non-interactive default is 127.0.0.1)
22+
# CODEMAN_PASSWORD - Preset the dashboard password (skips the
23+
# password prompt when binding to the network)
1824

1925
set -euo pipefail
2026

@@ -30,6 +36,13 @@ TARGET_NODE_VERSION="${CODEMAN_NODE_VERSION:-22}"
3036
NONINTERACTIVE="${CODEMAN_NONINTERACTIVE:-0}"
3137
SKIP_SYSTEMD="${CODEMAN_SKIP_SYSTEMD:-0}"
3238

39+
# Network binding chosen during install (choose_network_binding). Empty
40+
# BIND_HOST means "not chosen" (e.g. the update path) and falls back to the
41+
# server's own loopback default.
42+
BIND_HOST=""
43+
BIND_PASSWORD=""
44+
BIND_ACK="0"
45+
3346
# puppeteer is a devDependency used only by scripts/browser-comparison.mjs — its
3447
# ~150MB chrome-headless-shell download is never needed to build or run Codeman.
3548
# Skipping it avoids a slow download and a fatal install failure when a prior
@@ -131,16 +144,39 @@ die() {
131144
}
132145

133146
# Security notice — printed at the very end of install/update so it is the last
134-
# thing the user sees (the default loopback bind + how to expose it safely).
147+
# thing the user sees. Adapts to the binding chosen during install; the update
148+
# path (BIND_HOST empty) gets the generic text.
135149
print_security_notice() {
136150
echo ""
137-
echo -e " ${YELLOW}${BOLD}Security:${NC}"
138-
echo -e " Codeman binds ${BOLD}127.0.0.1${NC} (this machine only) — no password needed by default."
139-
echo -e " To reach it from another device, do ONE of:"
140-
echo -e " ${CYAN}${NC} tailscale serve / cloudflared tunnel ${DIM}(recommended)${NC}, or"
141-
echo -e " ${CYAN}${NC} ${CYAN}codeman web --host 0.0.0.0${NC} AND set ${CYAN}CODEMAN_PASSWORD${NC}"
142-
echo -e " A non-loopback bind without a password still starts, but warns loudly."
143-
echo -e " ${DIM}Details: docs/security-architecture.md${NC}"
151+
if [[ "$BIND_HOST" == "0.0.0.0" && -z "$BIND_PASSWORD" ]]; then
152+
echo -e " ${RED}${BOLD}============================================================${NC}"
153+
echo -e " ${RED}${BOLD} WARNING: NETWORK ACCESS WITHOUT A PASSWORD${NC}"
154+
echo -e " ${RED}${BOLD}============================================================${NC}"
155+
echo -e " ${RED}The dashboard is reachable by EVERY device on your network,${NC}"
156+
echo -e " ${RED}and whoever opens it can run commands as ${BOLD}$USER${NC}${RED} through${NC}"
157+
echo -e " ${RED}your AI agents. Anyone on your Wi-Fi owns this machine.${NC}"
158+
echo ""
159+
echo -e " Fix it by setting a password (takes 30 seconds):"
160+
echo -e " ${CYAN}${NC} re-run the installer and choose a password, or"
161+
echo -e " ${CYAN}${NC} add ${CYAN}Environment=CODEMAN_PASSWORD=<yours>${NC} to the service"
162+
echo -e " Or switch back to local-only: ${CYAN}CODEMAN_HOST=127.0.0.1${NC}"
163+
echo -e " ${DIM}Details: docs/security-architecture.md${NC}"
164+
elif [[ "$BIND_HOST" == "0.0.0.0" ]]; then
165+
echo -e " ${YELLOW}${BOLD}Security:${NC}"
166+
echo -e " The dashboard is reachable from your network at port 3000 and is"
167+
echo -e " password-protected (user ${BOLD}admin${NC}). Keep that password strong:"
168+
echo -e " whoever logs in can run commands through your agents."
169+
echo -e " For access from OUTSIDE your network, prefer Tailscale or a tunnel."
170+
echo -e " ${DIM}Details: docs/security-architecture.md${NC}"
171+
else
172+
echo -e " ${YELLOW}${BOLD}Security:${NC}"
173+
echo -e " Codeman binds ${BOLD}127.0.0.1${NC} (this machine only) — no password needed by default."
174+
echo -e " To reach it from another device, do ONE of:"
175+
echo -e " ${CYAN}${NC} tailscale serve / cloudflared tunnel ${DIM}(recommended)${NC}, or"
176+
echo -e " ${CYAN}${NC} ${CYAN}codeman web --host 0.0.0.0${NC} AND set ${CYAN}CODEMAN_PASSWORD${NC}"
177+
echo -e " A non-loopback bind without a password still starts, but warns loudly."
178+
echo -e " ${DIM}Details: docs/security-architecture.md${NC}"
179+
fi
144180
echo ""
145181
}
146182

@@ -781,6 +817,16 @@ read_reply() {
781817
fi
782818
}
783819

820+
read_secret() {
821+
# read_secret <varname>: like read_reply but without echoing (passwords)
822+
if [[ -t 0 ]]; then
823+
read -rs "$1"
824+
else
825+
read -rs "$1" < /dev/tty
826+
fi
827+
echo "" >&2
828+
}
829+
784830
# headless_guard <action>: refuse consequential system changes (sudo package
785831
# installs, third-party curl | bash installers) when nobody can consent, i.e.
786832
# no terminal AND no explicit CODEMAN_NONINTERACTIVE=1 opt-in. Interactive
@@ -926,6 +972,117 @@ setup_sc_alias() {
926972
info "Added 'sc' alias for tmux-chooser"
927973
}
928974

975+
# ============================================================================
976+
# Network Binding
977+
# ============================================================================
978+
979+
# Best-effort LAN IP for "open this URL from your phone" hints.
980+
detect_lan_ip() {
981+
local ip=""
982+
if [[ "$(uname -s)" == "Darwin" ]]; then
983+
ip=$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)
984+
else
985+
ip=$(hostname -I 2>/dev/null | awk '{print $1}')
986+
fi
987+
echo "${ip:-<your-ip>}"
988+
}
989+
990+
# Escape a value for a quoted systemd Environment="KEY=value" assignment.
991+
systemd_env_escape() {
992+
printf '%s' "$1" | sed 's/[\\"]/\\&/g'
993+
}
994+
995+
# Escape a value for embedding in a launchd plist <string>.
996+
xml_escape() {
997+
printf '%s' "$1" | sed -e 's/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/g'
998+
}
999+
1000+
# Ask how the dashboard should be reachable and set BIND_HOST/BIND_PASSWORD/
1001+
# BIND_ACK. Interactive default is network access (0.0.0.0) because that is
1002+
# what most installs need; loopback is offered as the safer alternative.
1003+
# Non-interactive runs keep the safe loopback default unless CODEMAN_HOST is
1004+
# preset. The server binary itself still defaults to 127.0.0.1 either way.
1005+
choose_network_binding() {
1006+
# Preset via environment: honor it and skip the prompt entirely.
1007+
if [[ -n "${CODEMAN_HOST:-}" ]]; then
1008+
BIND_HOST="$CODEMAN_HOST"
1009+
BIND_PASSWORD="${CODEMAN_PASSWORD:-}"
1010+
if [[ "$BIND_HOST" != "127.0.0.1" && -z "$BIND_PASSWORD" ]]; then
1011+
BIND_ACK="1"
1012+
fi
1013+
info "Network binding preset via CODEMAN_HOST: $BIND_HOST"
1014+
return 0
1015+
fi
1016+
1017+
if [[ "$NONINTERACTIVE" == "1" ]] || ! has_tty; then
1018+
BIND_HOST="127.0.0.1"
1019+
info "Non-interactive install: binding 127.0.0.1 (preset CODEMAN_HOST=0.0.0.0 to override)"
1020+
return 0
1021+
fi
1022+
1023+
echo -e " ${BOLD}Network access${NC}"
1024+
echo ""
1025+
echo -e " How should the Codeman dashboard be reachable?"
1026+
echo ""
1027+
echo -e " ${CYAN}1)${NC} ${BOLD}Any device on your network${NC} ${DIM}(0.0.0.0)${NC} [default]"
1028+
echo -e " Open it straight from your phone or laptop."
1029+
echo -e " ${YELLOW}Less safe: set a password so only you control your agents.${NC}"
1030+
echo -e " ${CYAN}2)${NC} ${BOLD}This machine only${NC} ${DIM}(127.0.0.1)${NC}"
1031+
echo -e " Safest. Reach it remotely via Tailscale or a tunnel."
1032+
echo ""
1033+
1034+
local bind_choice=""
1035+
while true; do
1036+
echo -en "${CYAN}Choose [1/2] (default 1):${NC} " >&2
1037+
read_reply bind_choice || bind_choice="1"
1038+
bind_choice="${bind_choice:-1}"
1039+
case "$bind_choice" in
1040+
1|2) break ;;
1041+
*) echo "Please enter 1 or 2." >&2 ;;
1042+
esac
1043+
done
1044+
1045+
if [[ "$bind_choice" == "2" ]]; then
1046+
BIND_HOST="127.0.0.1"
1047+
success "Binding 127.0.0.1 (this machine only)"
1048+
return 0
1049+
fi
1050+
1051+
BIND_HOST="0.0.0.0"
1052+
1053+
if [[ -n "${CODEMAN_PASSWORD:-}" ]]; then
1054+
BIND_PASSWORD="$CODEMAN_PASSWORD"
1055+
info "Using CODEMAN_PASSWORD from the environment"
1056+
return 0
1057+
fi
1058+
1059+
echo ""
1060+
local pw="" pw2=""
1061+
while true; do
1062+
echo -en "${CYAN}Set a dashboard password (recommended; Enter to skip):${NC} " >&2
1063+
read_secret pw || pw=""
1064+
if [[ -z "$pw" ]]; then
1065+
echo ""
1066+
warn "Without a password, EVERY device on your network gets full access"
1067+
warn "to your agents (they run commands as $USER)."
1068+
if prompt_yes_no "Continue WITHOUT a password?" "n"; then
1069+
BIND_ACK="1"
1070+
break
1071+
fi
1072+
continue
1073+
fi
1074+
echo -en "${CYAN}Confirm password:${NC} " >&2
1075+
read_secret pw2 || pw2=""
1076+
if [[ "$pw" == "$pw2" ]]; then
1077+
BIND_PASSWORD="$pw"
1078+
success "Password set (login user: admin)"
1079+
break
1080+
fi
1081+
echo "Passwords do not match, try again." >&2
1082+
done
1083+
return 0
1084+
}
1085+
9291086
# ============================================================================
9301087
# Service Setup (Linux systemd / macOS launchd)
9311088
# ============================================================================
@@ -976,6 +1133,21 @@ setup_launchd_service() {
9761133
local node_path
9771134
node_path=$(command -v node)
9781135

1136+
# Binding chosen during install (empty on paths that never asked)
1137+
local bind_plist=""
1138+
if [[ -n "$BIND_HOST" ]]; then
1139+
bind_plist=" <key>CODEMAN_HOST</key>
1140+
<string>$BIND_HOST</string>"
1141+
if [[ -n "$BIND_PASSWORD" ]]; then
1142+
bind_plist+=$'\n'" <key>CODEMAN_PASSWORD</key>
1143+
<string>$(xml_escape "$BIND_PASSWORD")</string>"
1144+
fi
1145+
if [[ "$BIND_ACK" == "1" ]]; then
1146+
bind_plist+=$'\n'" <key>CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK</key>
1147+
<string>1</string>"
1148+
fi
1149+
fi
1150+
9791151
cat > "$agent_plist" << EOF
9801152
<?xml version="1.0" encoding="UTF-8"?>
9811153
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
@@ -997,6 +1169,7 @@ setup_launchd_service() {
9971169
<string>$HOME</string>
9981170
<key>LANG</key>
9991171
<string>en_US.UTF-8</string>
1172+
$bind_plist
10001173
</dict>
10011174
<key>WorkingDirectory</key>
10021175
<string>$HOME</string>
@@ -1039,6 +1212,18 @@ setup_systemd_service() {
10391212
local node_path
10401213
node_path=$(command -v node)
10411214

1215+
# Binding chosen during install (empty on paths that never asked)
1216+
local bind_env=""
1217+
if [[ -n "$BIND_HOST" ]]; then
1218+
bind_env="Environment=CODEMAN_HOST=$BIND_HOST"
1219+
if [[ -n "$BIND_PASSWORD" ]]; then
1220+
bind_env+=$'\n'"Environment=\"CODEMAN_PASSWORD=$(systemd_env_escape "$BIND_PASSWORD")\""
1221+
fi
1222+
if [[ "$BIND_ACK" == "1" ]]; then
1223+
bind_env+=$'\n'"Environment=CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1"
1224+
fi
1225+
fi
1226+
10421227
# Create service file
10431228
cat > "$service_file" << EOF
10441229
[Unit]
@@ -1053,6 +1238,7 @@ Restart=always
10531238
RestartSec=10
10541239
Environment=NODE_ENV=production
10551240
Environment=PATH=$PATH
1241+
$bind_env
10561242
10571243
[Install]
10581244
WantedBy=default.target
@@ -1419,6 +1605,11 @@ main() {
14191605
echo -e "${GREEN}${BOLD}============================================================${NC}"
14201606
echo ""
14211607

1608+
# Ask how the dashboard should be reachable BEFORE the launch menu, so the
1609+
# service files and the run-now path all inherit the choice.
1610+
choose_network_binding
1611+
echo ""
1612+
14221613
local launch_choice=""
14231614
local has_service=false
14241615
local service_type=""
@@ -1506,7 +1697,12 @@ main() {
15061697
echo -e " ${GREEN}${BOLD}Codeman is running now!${NC}"
15071698
echo ""
15081699
echo -e " ${CYAN}# Open in browser${NC}"
1509-
echo -e " http://localhost:3000"
1700+
if [[ "$BIND_HOST" == "0.0.0.0" ]]; then
1701+
echo -e " http://$(detect_lan_ip):3000 ${DIM}(any device on your network)${NC}"
1702+
echo -e " http://localhost:3000 ${DIM}(this machine)${NC}"
1703+
else
1704+
echo -e " http://localhost:3000"
1705+
fi
15101706
else
15111707
echo -e " ${YELLOW}${BOLD}The service was set up but is not running yet${NC} (see warnings above)."
15121708
echo -e " ${DIM}You can always run it directly:${NC} ${CYAN}codeman web${NC}"
@@ -1531,11 +1727,23 @@ main() {
15311727
if [[ "$launch_choice" != "2" ]]; then
15321728
echo -e " ${BOLD}Quick Start:${NC}"
15331729
echo ""
1534-
echo -e " ${CYAN}codeman web${NC} # Start the web server"
1535-
echo -e " ${CYAN}codeman web --https${NC} # With HTTPS (for remote access)"
1536-
echo ""
1537-
echo -e " ${CYAN}# Open in browser${NC}"
1538-
echo -e " http://localhost:3000"
1730+
if [[ "$BIND_HOST" == "0.0.0.0" ]]; then
1731+
if [[ -n "$BIND_PASSWORD" ]]; then
1732+
echo -e " ${CYAN}CODEMAN_HOST=0.0.0.0 CODEMAN_PASSWORD='<your-password>' codeman web${NC}"
1733+
else
1734+
echo -e " ${CYAN}CODEMAN_HOST=0.0.0.0 codeman web${NC}"
1735+
fi
1736+
echo -e " ${DIM}(a bare 'codeman web' binds 127.0.0.1, this machine only)${NC}"
1737+
echo ""
1738+
echo -e " ${CYAN}# Open in browser${NC}"
1739+
echo -e " http://$(detect_lan_ip):3000 ${DIM}(any device on your network)${NC}"
1740+
else
1741+
echo -e " ${CYAN}codeman web${NC} # Start the web server"
1742+
echo -e " ${CYAN}codeman web --https${NC} # With HTTPS (for remote access)"
1743+
echo ""
1744+
echo -e " ${CYAN}# Open in browser${NC}"
1745+
echo -e " http://localhost:3000"
1746+
fi
15391747
echo ""
15401748
fi
15411749

@@ -1584,6 +1792,11 @@ main() {
15841792
# Source profile to pick up PATH changes, then exec codeman
15851793
# shellcheck disable=SC1090
15861794
source "$profile" 2>/dev/null || true
1795+
if [[ -n "$BIND_HOST" ]]; then
1796+
export CODEMAN_HOST="$BIND_HOST"
1797+
[[ -n "$BIND_PASSWORD" ]] && export CODEMAN_PASSWORD="$BIND_PASSWORD"
1798+
[[ "$BIND_ACK" == "1" ]] && export CODEMAN_ALLOW_UNAUTHENTICATED_NETWORK=1
1799+
fi
15871800
exec node "$INSTALL_DIR/dist/index.js" web
15881801
fi
15891802
}

0 commit comments

Comments
 (0)