#!/usr/bin/env bash # claude-egress-guard — one-shot installer. # # From a checkout: sudo ./install.sh --proxy URL --exit-ip ADDR # From the network: curl -fsSL https://github.com/yinyues2/claude-egress-guard/raw/main/install.sh \ # | sudo bash -s -- --proxy URL --exit-ip ADDR # # The script downloads a pinned release tarball when it is not run from a # checkout, verifies the expected layout, then hands over to the project CLI. # It never edits system files itself. set -u REPO="yinyues2/claude-egress-guard" PINNED_REF="v1.4.0" SELF_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P) WORK="" cleanup() { [ -n "$WORK" ] && rm -rf "$WORK"; } trap cleanup EXIT say() { printf '%s\n' "$*"; } err() { printf 'error: %s\n' "$*" >&2; } die() { err "$*" exit 1 } usage() { cat <<'EOF' claude-egress-guard installer USAGE install.sh --proxy URL --exit-ip ADDR [options] install.sh --skill-only [--dest DIR] install.sh --uninstall [--prefix DIR] OPTIONS --proxy URL local HTTP/mixed proxy endpoint (required to install) --exit-ip ADDR public IPv4 the proxy must exit from (required) --country CC two-letter country to enforce (optional) --allow LIST extra CIDRs inside the kernel jail, comma separated --strict-net MODE on|off kernel jail (default on) --native-path PATH also gate the vendor installer's binary, e.g. ~/.local/bin/claude (it wins over /usr/local/bin in PATH) --prefix DIR install under DIR instead of / (testing) --ref REF release tag or branch to download (default: the pinned tag) --dry-run show the install plan only --yes do not prompt --skill-only copy the agent skill into a skills directory instead --dest DIR target for --skill-only (default: ~/.hermes/skills) --uninstall remove the gate and restore the previous files -h, --help this text NOTES The installer needs root for a real install because the gate writes to /usr/local and /etc. --prefix works unprivileged and is what the test suite uses. Nothing on the host is modified before the CLI prints its plan. EOF } PROXY="" EXIT_IP="" COUNTRY="" ALLOW="" STRICT="" NATIVE_PATH="" PREFIX="" REF="$PINNED_REF" DRY="0" ASSUME_YES="0" SKILL_ONLY="0" DEST="${HOME}/.hermes/skills" UNINSTALL="0" while [ $# -gt 0 ]; do case "$1" in --proxy) PROXY="${2:-}"; shift 2 ;; --exit-ip) EXIT_IP="${2:-}"; shift 2 ;; --country) COUNTRY="${2:-}"; shift 2 ;; --allow) ALLOW="${2:-}"; shift 2 ;; --strict-net) STRICT="${2:-}"; shift 2 ;; --native-path) NATIVE_PATH="${2:-}"; shift 2 ;; --prefix) PREFIX="${2:-}"; shift 2 ;; --ref) REF="${2:-}"; shift 2 ;; --dry-run) DRY="1"; shift ;; --yes | -y) ASSUME_YES="1"; shift ;; --skill-only) SKILL_ONLY="1"; shift ;; --dest) DEST="${2:-}"; shift 2 ;; --uninstall) UNINSTALL="1"; shift ;; -h | --help) usage; exit 0 ;; *) die "unknown option: $1 (try --help)" ;; esac done # --------------------------------------------------------------------------- # Locate the project: a checkout next to this script, or a downloaded tarball. # --------------------------------------------------------------------------- project_root() { if [ -x "$SELF_DIR/skills/claude-egress-guard/scripts/claude-egress-guard" ]; then printf '%s\n' "$SELF_DIR" return 0 fi command -v curl >/dev/null 2>&1 || die "curl is required to download the project" command -v tar >/dev/null 2>&1 || die "tar is required to unpack the project" WORK=$(mktemp -d "${TMPDIR:-/tmp}/ceg-install.XXXXXX") || die "cannot create a temp dir" local url case "$REF" in main | master | [0-9]*) url="https://codeload.github.com/$REPO/tar.gz/refs/heads/$REF" ;; *) url="https://codeload.github.com/$REPO/tar.gz/refs/tags/$REF" ;; esac say "downloading $REPO@$REF" curl -fsSL --max-time 120 --retry 2 "$url" -o "$WORK/src.tar.gz" || die "download failed: $url" tar -xzf "$WORK/src.tar.gz" -C "$WORK" || die "cannot unpack the release tarball" local root root=$(find "$WORK" -maxdepth 1 -mindepth 1 -type d -name 'claude-egress-guard-*' | head -n 1) [ -n "$root" ] || die "unexpected archive layout" [ -x "$root/skills/claude-egress-guard/scripts/claude-egress-guard" ] || die "archive does not contain the expected scripts" printf '%s\n' "$root" } confirm() { [ "$ASSUME_YES" = "1" ] && return 0 if [ -r /dev/tty ]; then local answer="" printf '%s [y/N] ' "$1" >/dev/tty read -r answer " say " journalctl -t claude-egress-guard -f # when LOG_REFUSALS=1"