diff --git a/AGENTS.md b/AGENTS.md
index 9a10010..95e9bc4 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -2,14 +2,14 @@
 
 ## Project Structure & Module Organization
 
-This is a Bash CLI project. The active runtime is the root `devloop` executable. `VERSION` is the single version source, `release.sh` cuts local release commits and annotated tags, `install.sh` links the CLI into a local bin directory and installs bundled skills into `~/.agents/skills` and `~/.claude/skills`, `tests/devloop_test.sh` covers the shell runtime, `skills/devloop-spec/SKILL.md` is the spec-generation skill, `skills/devloop-review/SKILL.md` is the review skill, and `skills/devloop-spec/references/spec-template.md` is the starter spec. Generated runtime output belongs under `.codex/` in target repositories and should not be committed here.
+This is a Bash CLI project. The active runtime is the root `devloop` executable. `VERSION` is the single version source, `scripts/release.sh` cuts local release commits and annotated tags, `scripts/install.sh` links the CLI into a local bin directory and installs bundled skills into `~/.agents/skills` and `~/.claude/skills`, `scripts/devloop_test.sh` covers the shell runtime, `skills/devloop-spec/SKILL.md` is the spec-generation skill, `skills/devloop-review/SKILL.md` is the review skill, and `skills/devloop-spec/references/spec-template.md` is the starter spec. Generated runtime output belongs under `.codex/` in target repositories and should not be committed here.
 
 ## Build, Test, and Development Commands
 
-- `bash tests/devloop_test.sh`: run the shell test suite.
-- `./install.sh`: link `devloop` into `~/.local/bin` or `DEVLOOP_BIN_DIR`.
+- `bash scripts/devloop_test.sh`: run the shell test suite.
+- `./scripts/install.sh`: link `devloop` into `~/.local/bin` or `DEVLOOP_BIN_DIR`.
 - `./devloop --plain .specs/change.md`: example local CLI invocation from a target git worktree.
-- `./release.sh patch --dry-run`: validate the release path without changing files.
+- `./scripts/release.sh patch --dry-run`: validate the release path without changing files.
 
 ## Coding Style & Naming Conventions
 
@@ -17,11 +17,11 @@ Use readable Bash with small named functions, quoted expansions, explicit status
 
 ## Testing Guidelines
 
-Tests use `tests/devloop_test.sh`. Keep behavior fixture-style where possible: assert generated files, git state, status codes, and user-visible output.
+Tests use `scripts/devloop_test.sh`. Keep behavior fixture-style where possible: assert generated files, git state, status codes, and user-visible output.
 
 ## Commit & Pull Request Guidelines
 
-Git history follows Conventional Commits, for example `fix: surface devloop commit failures` and `chore: cover shell runtime`. Use concise subjects in imperative style and keep unrelated changes in separate commits. Pull requests should include a short problem summary, the implementation approach, and test evidence (`bash tests/devloop_test.sh`).
+Git history follows Conventional Commits, for example `fix: surface devloop commit failures` and `chore: cover shell runtime`. Use concise subjects in imperative style and keep unrelated changes in separate commits. Pull requests should include a short problem summary, the implementation approach, and test evidence (`bash scripts/devloop_test.sh`).
 
 ## Agent-Specific Instructions
 
diff --git a/README.md b/README.md
index 8a7b03f..0668bf4 100644
--- a/README.md
+++ b/README.md
@@ -8,16 +8,48 @@ By default, Codex makes the change, Claude Code reviews it, and Codex retries un
 
 ## Install
 
+Install Devloop with the remote installer:
+
+```sh
+curl -fsSL https://devloop.sh/install | bash
+```
+
+The installer downloads a tagged GitHub release asset, verifies its `.sha256` checksum, installs it under `~/.local/share/devloop/<version>/`, links `~/.local/bin/devloop`, and installs the bundled Agent Skills for Codex under `~/.agents/skills` and Claude Code under `~/.claude/skills`.
+
+Install a specific version or preview the install:
+
+```sh
+curl -fsSL https://devloop.sh/install | bash -s -- --version 0.2.0
+curl -fsSL https://devloop.sh/install | bash -s -- --dry-run
+curl -fsSL https://devloop.sh/install | bash -s -- --no-skills
+```
+
+Inspect before running:
+
+```sh
+curl -fsSL https://devloop.sh/install -o install.sh
+less install.sh
+bash install.sh
+```
+
+The remote installer checks for `gum`, `fzf`, `codex`, and `claude`. It does not install Codex or Claude Code. If `gum` or `fzf` is missing, it only installs them with Homebrew after explicit confirmation; otherwise it prints the `brew install gum fzf` command to run yourself.
+
+If `~/.local/bin` is not on `PATH`, the installer prints the shell profile line to add. It does not edit shell profile files.
+
 Required dependencies: Bash, git, Homebrew, `codex`, `claude`, `gum`, and `fzf`.
-`install.sh` installs missing `gum` and `fzf` with Homebrew. Install the Codex and Claude Code CLIs before running a loop, then verify everything with `devloop doctor`.
+`scripts/install.sh` installs missing `gum` and `fzf` with Homebrew. Install the Codex and Claude Code CLIs before running a loop, then verify everything with `devloop doctor`.
 The GitHub CLI is optional. Install `gh` and run `gh auth login` only when you want PR-backed loops.
 
+For source checkout development, use the local installer:
+
 ```sh
 git clone https://github.com/satyaborg/devloop.git
 cd devloop
-./install.sh
+./scripts/install.sh
 ```
 
+`./scripts/install.sh` symlinks the checkout executable, installs missing `gum` and `fzf` with Homebrew when available, installs bundled skills, and finishes with `try: devloop doctor`.
+
 Check the installed version:
 
 ```sh
@@ -30,14 +62,21 @@ Run without installing:
 ./devloop --help
 ```
 
-`install.sh` also installs the bundled Agent Skills globally for Codex under `~/.agents/skills`
-and Claude Code under `~/.claude/skills`.
 After install or update, verify the local setup:
 
 ```sh
 devloop doctor
 ```
 
+Uninstall a remote install:
+
+```sh
+rm -f ~/.local/bin/devloop
+rm -rf ~/.local/share/devloop
+rm -rf ~/.agents/skills/devloop-spec ~/.agents/skills/devloop-review
+rm -rf ~/.claude/skills/devloop-spec ~/.claude/skills/devloop-review
+```
+
 ## Quick Start
 
 Create a spec:
@@ -146,7 +185,7 @@ When stdout is a terminal, running `devloop` without arguments opens a menu:
 
 Nested menu screens keep `Back` as the final option, and Esc/cancel also returns to the previous menu without exiting Devloop. Interactive screens redraw in place instead of appending a fresh UI after each selection.
 
-`gum` powers the branded help screen, prompts, confirmations, status output, paging, and setup screens. `fzf` powers searchable pickers for specs, tracks, and reports. Both are required and installed by `install.sh` when missing.
+`gum` powers the branded help screen, prompts, confirmations, status output, paging, and setup screens. `fzf` powers searchable pickers for specs, tracks, and reports. Both are required and installed by `scripts/install.sh` when missing.
 
 ## What Devloop Does
 
@@ -178,18 +217,18 @@ If present, `.devloop/verify` is executed from the run worktree with the pass nu
 ## Development
 
 ```sh
-bash -n devloop install.sh release.sh skill_helpers.sh
-shellcheck devloop install.sh skill_helpers.sh release.sh tests/devloop_test.sh
-bash tests/devloop_test.sh
+bash -n devloop scripts/install.sh scripts/release.sh scripts/skill_helpers.sh scripts/install.remote.sh scripts/devloop_test.sh
+shellcheck devloop scripts/install.sh scripts/skill_helpers.sh scripts/release.sh scripts/install.remote.sh scripts/devloop_test.sh
+bash scripts/devloop_test.sh
 ./devloop --help
 ./devloop --version
 tmp="$(mktemp -d)"
-DEVLOOP_BIN_DIR="$tmp/bin" HOME="$tmp/home" ./install.sh
+DEVLOOP_BIN_DIR="$tmp/bin" HOME="$tmp/home" ./scripts/install.sh
 PATH="$tmp/bin:$PATH" HOME="$tmp/home" devloop doctor
 ```
 
 The supported runtime is the root [`devloop`](devloop) Bash script.
-The shell suite enforces 100% project function coverage for `devloop`, `skill_helpers.sh`, and `release.sh`.
+The shell suite enforces 100% project function coverage for `devloop`, `scripts/skill_helpers.sh`, and `scripts/release.sh`.
 
 ## Versioning and Release
 
@@ -205,11 +244,11 @@ brew install gh
 Cut a release from a clean tree by choosing the bump:
 
 ```sh
-./release.sh patch --dry-run
-./release.sh patch --publish
+./scripts/release.sh patch --dry-run
+./scripts/release.sh patch --publish
 ```
 
-Use `patch`, `minor`, or `major`. The script reads the current [`VERSION`](VERSION), computes the next SemVer version, updates `VERSION` and [`CHANGELOG.md`](CHANGELOG.md), runs `bash tests/devloop_test.sh`, commits `chore: release <version>`, and creates an annotated `v<version>` tag. Add `--publish` to push the release branch and tag, then create the GitHub Release. Use `--push` only when you want to publish the git refs without creating a GitHub Release. By default, published releases must run from `main`.
+Use `patch`, `minor`, or `major`. The script reads the current [`VERSION`](VERSION), computes the next SemVer version, updates `VERSION` and [`CHANGELOG.md`](CHANGELOG.md), runs `bash scripts/devloop_test.sh`, commits `chore: release <version>`, and creates an annotated `v<version>` tag. Add `--publish` to push the release branch and tag, then create the GitHub Release. Use `--push` only when you want to publish the git refs without creating a GitHub Release. By default, published releases must run from `main`.
 
 ## License
 
diff --git a/devloop b/devloop
index e8d5b76..090be34 100755
--- a/devloop
+++ b/devloop
@@ -18,7 +18,7 @@ while [ -L "$SCRIPT_PATH" ]; do
   esac
 done
 ROOT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)"
-source "$ROOT_DIR/skill_helpers.sh"
+source "$ROOT_DIR/scripts/skill_helpers.sh"
 DEVLOOP_VERSION="$(sed -n '1p' "$ROOT_DIR/VERSION" 2>/dev/null || true)"
 if [ -z "$DEVLOOP_VERSION" ]; then DEVLOOP_VERSION="0.0.0-dev"; fi
 
diff --git a/tests/devloop_test.sh b/scripts/devloop_test.sh
similarity index 85%
rename from tests/devloop_test.sh
rename to scripts/devloop_test.sh
index b5afe14..4a9354a 100755
--- a/tests/devloop_test.sh
+++ b/scripts/devloop_test.sh
@@ -1,7 +1,10 @@
 #!/usr/bin/env bash
+# shellcheck disable=SC2030,SC2031
 set -euo pipefail
 
 REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
+SCRIPTS_DIR="$REPO_ROOT/scripts"
+REMOTE_INSTALLER="$SCRIPTS_DIR/install.remote.sh"
 
 fail() {
   echo "not ok - $*" >&2
@@ -33,7 +36,7 @@ equals() {
   [[ "$actual" == "$expected" ]] || fail "$label expected [$expected], got [$actual]"
 }
 
-bash -n "$REPO_ROOT/devloop" "$REPO_ROOT/install.sh" "$REPO_ROOT/skill_helpers.sh" "$REPO_ROOT/release.sh"
+bash -n "$REPO_ROOT/devloop" "$SCRIPTS_DIR/install.sh" "$SCRIPTS_DIR/skill_helpers.sh" "$SCRIPTS_DIR/release.sh" "$REMOTE_INSTALLER"
 ok "bash syntax"
 
 DEVLOOP_LIB=1
@@ -61,6 +64,28 @@ contains "$help" "--version" "help"
 contains "$help" "--timeout-minutes" "help"
 ok "help output"
 
+remote_help="$("$REMOTE_INSTALLER" --help)"
+contains "$remote_help" "curl -fsSL https://devloop.sh/install | bash" "remote installer help"
+contains "$remote_help" "--yes" "remote installer help"
+contains "$remote_help" "--version <version>" "remote installer help"
+contains "$remote_help" "--no-skills" "remote installer help"
+contains "$remote_help" "--dry-run" "remote installer help"
+contains "$remote_help" "--install-dir <dir>" "remote installer help"
+contains "$remote_help" "--bin-dir <dir>" "remote installer help"
+ok "remote installer help output"
+
+readme_text="$(cat "$REPO_ROOT/README.md")"
+tilde_marker="~"
+contains "$readme_text" "curl -fsSL https://devloop.sh/install | bash" "README remote install"
+contains "$readme_text" "git clone https://github.com/satyaborg/devloop.git" "README source install"
+contains "$readme_text" "cd devloop" "README source install"
+contains "$readme_text" "./scripts/install.sh" "README source install"
+contains "$readme_text" "rm -f ~/.local/bin/devloop" "README uninstall"
+contains "$readme_text" "rm -rf ~/.local/share/devloop" "README uninstall"
+contains "$readme_text" "$tilde_marker/.agents/skills/devloop-spec" "README uninstall"
+contains "$readme_text" "$tilde_marker/.claude/skills/devloop-review" "README uninstall"
+ok "README install and uninstall docs"
+
 skill_path="$("$REPO_ROOT/devloop" spec --skill-path)"
 [[ "$skill_path" == "$REPO_ROOT/skills/devloop-spec/SKILL.md" ]] || fail "unexpected skill path: $skill_path"
 contains "$("$REPO_ROOT/devloop" spec --print-skill)" "name: devloop-spec" "spec skill"
@@ -94,11 +119,26 @@ ok "skill metadata"
 work=$(mktemp -d "${TMPDIR:-/tmp}/devloop-test.XXXXXX")
 trap 'rm -rf "$work"' EXIT
 
+make_remote_release() {
+  local version="$1"
+  local releases="$2"
+  local fixture="$work/remote-release-src-$version"
+  local release_dir="$releases/v$version"
+  local archive="$release_dir/devloop-$version.tar.gz"
+  mkdir -p "$fixture/devloop-$version/scripts" "$release_dir"
+  cp "$REPO_ROOT/devloop" "$fixture/devloop-$version/devloop"
+  cp "$SCRIPTS_DIR/skill_helpers.sh" "$fixture/devloop-$version/scripts/skill_helpers.sh"
+  cp -R "$REPO_ROOT/skills" "$fixture/devloop-$version/skills"
+  printf '%s\n' "$version" > "$fixture/devloop-$version/VERSION"
+  tar -C "$fixture" -czf "$archive" "devloop-$version"
+  printf '%s  %s\n' "$(devloop_checksum_file "$archive")" "devloop-$version.tar.gz" > "$archive.sha256"
+}
+
 coverage_functions="$work/project-functions.txt"
 coverage_hits="$work/project-function-hits.txt"
 coverage_set=""
 sed -nE 's/^([[:alpha:]_][[:alnum:]_]*)\(\)[[:space:]]*\{/\1/p' \
-  "$REPO_ROOT/devloop" "$REPO_ROOT/skill_helpers.sh" "$REPO_ROOT/release.sh" |
+  "$REPO_ROOT/devloop" "$SCRIPTS_DIR/skill_helpers.sh" "$SCRIPTS_DIR/release.sh" |
   LC_ALL=C sort -u > "$coverage_functions"
 while IFS= read -r fn; do
   coverage_set="${coverage_set}|${fn}|"
@@ -469,18 +509,29 @@ ok "pure helpers"
 
 (
   DEVLOOP_RELEASE_LIB=1
-  source "$REPO_ROOT/release.sh"
-  contains "$(release_usage)" "usage: ./release.sh" "release usage"
+  export DEVLOOP_RELEASE_LIB
+  # shellcheck disable=SC1091
+  source "$SCRIPTS_DIR/release.sh"
+  contains "$(release_usage)" "usage: ./scripts/release.sh" "release usage"
   release_version_valid "0.1.0" || fail "release version rejected valid patch"
   release_version_valid "1.2.3-alpha.1+build.7" || fail "release version rejected valid prerelease"
   if release_version_valid "01.2.3"; then fail "release version accepted leading zero"; fi
   if release_version_valid "1.2"; then fail "release version accepted missing patch"; fi
   if release_version_valid "1.2.3-alpha.01"; then fail "release version accepted leading zero prerelease"; fi
   equals "$(release_tag_for_version "1.2.3")" "v1.2.3" "release tag"
+  release_artifact_dir="$work/release-artifacts"
+  release_create_artifacts "$version" "$release_artifact_dir"
+  [[ -f "$RELEASE_ARCHIVE" ]] || fail "release archive was not created"
+  [[ -f "$RELEASE_CHECKSUM" ]] || fail "release checksum was not created"
+  contains "$(tar -tzf "$RELEASE_ARCHIVE")" "devloop-$version/devloop" "release archive"
+  contains "$(tar -tzf "$RELEASE_ARCHIVE")" "devloop-$version/scripts/devloop_test.sh" "release archive"
+  contains "$(tar -tzf "$RELEASE_ARCHIVE")" "devloop-$version/scripts/install.remote.sh" "release archive"
+  equals "$(awk '{print $1; exit}' "$RELEASE_CHECKSUM")" "$(release_checksum_file "$RELEASE_ARCHIVE")" "release checksum"
   equals "$(release_next_version patch "0.1.0")" "0.1.1" "patch bump"
   equals "$(release_next_version minor "0.1.0")" "0.2.0" "minor bump"
   equals "$(release_next_version major "0.1.0")" "1.0.0" "major bump"
   if release_next_version patch "0.1.0-alpha.1" >/dev/null 2>&1; then fail "release bump accepted prerelease"; fi
+  # shellcheck disable=SC2329
   release_require_command() {
     if [ "$1" = "git-cliff" ]; then return 1; fi
     return 0
@@ -494,6 +545,7 @@ ok "pure helpers"
   contains "$dry_run_output" "would tag: v9.9.10" "release dry-run"
   publish_dry_run_output="$(release_main "patch" --publish --dry-run)" || fail "release publish dry-run required git-cliff"
   contains "$publish_dry_run_output" "would push branch and tag" "release publish dry-run"
+  contains "$publish_dry_run_output" "would build release assets: devloop-9.9.10.tar.gz and devloop-9.9.10.tar.gz.sha256" "release publish dry-run"
   contains "$publish_dry_run_output" "would create GitHub release: gh release create v9.9.10 --verify-tag --generate-notes" "release publish dry-run"
   git -C "$ROOT" config user.email devloop-test@example.com
   git -C "$ROOT" config user.name "devloop test"
@@ -508,6 +560,141 @@ ok "pure helpers"
 )
 ok "release helpers"
 
+remote_version="9.8.7"
+remote_releases="$work/remote-releases"
+make_remote_release "$remote_version" "$remote_releases"
+remote_release_base="file://$remote_releases"
+remote_no_tools="$work/remote-no-tools"
+mkdir -p "$remote_no_tools"
+remote_no_tools_path="$remote_no_tools:/usr/bin:/bin:/usr/sbin:/sbin"
+
+remote_custom_root="$work/remote-custom-root"
+remote_custom_bin="$work/remote-custom-bin"
+remote_dry_output="$(
+  HOME="$work/remote-dry-home" PATH="$remote_no_tools_path" bash "$REMOTE_INSTALLER" \
+    --dry-run \
+    --version "$remote_version" \
+    --install-dir "$remote_custom_root" \
+    --bin-dir "$remote_custom_bin" \
+    --release-base-url "$remote_release_base" \
+    2>&1
+)"
+contains "$remote_dry_output" "dry run: no files will be changed" "remote dry run"
+contains "$remote_dry_output" "version: $remote_version" "remote dry run version"
+contains "$remote_dry_output" "download: $remote_release_base/v$remote_version/devloop-$remote_version.tar.gz" "remote dry run download"
+contains "$remote_dry_output" "verify: $remote_release_base/v$remote_version/devloop-$remote_version.tar.gz.sha256" "remote dry run checksum"
+contains "$remote_dry_output" "install: $remote_custom_root/$remote_version" "remote dry run install dir"
+contains "$remote_dry_output" "link: $remote_custom_bin/devloop -> $remote_custom_root/$remote_version/devloop" "remote dry run bin dir"
+contains "$remote_dry_output" "skills: $work/remote-dry-home/.agents/skills, $work/remote-dry-home/.claude/skills" "remote dry run skills"
+contains "$remote_dry_output" "missing UI tools: gum fzf" "remote missing UI guidance"
+contains "$remote_dry_output" "install with: brew install gum fzf" "remote missing UI guidance"
+contains "$remote_dry_output" "missing agent CLIs: codex claude" "remote missing agent guidance"
+contains "$remote_dry_output" "Devloop does not install codex or claude automatically." "remote missing agent guidance"
+[[ ! -e "$remote_custom_root" ]] || fail "remote dry run created install root"
+[[ ! -e "$remote_custom_bin" ]] || fail "remote dry run created bin dir"
+ok "remote installer dry run"
+
+latest_api_file="$work/latest-release.json"
+latest_tool_path="$work/latest-tool-path"
+test_bash="$(command -v bash)"
+mkdir -p "$latest_tool_path"
+for tool in grep sed head; do
+  ln -s "$(command -v "$tool")" "$latest_tool_path/$tool"
+done
+printf '{"tag_name":"v%s"}\n' "$remote_version" > "$latest_api_file"
+if ! remote_latest_output="$(
+  HOME="$work/remote-latest-home" PATH="$latest_tool_path" DEVLOOP_GITHUB_API_URL="file://$latest_api_file" "$test_bash" "$REMOTE_INSTALLER" \
+    --dry-run \
+    --release-base-url "$remote_release_base" \
+    2>&1
+)"; then
+  printf '%s\n' "$remote_latest_output" >&2
+  fail "remote latest version dry run failed"
+fi
+contains "$remote_latest_output" "version: $remote_version" "remote latest version"
+contains "$remote_latest_output" "download: $remote_release_base/v$remote_version/devloop-$remote_version.tar.gz" "remote latest version"
+ok "remote installer latest version resolution"
+
+tampered_version="9.8.8"
+tampered_releases="$work/tampered-releases"
+make_remote_release "$tampered_version" "$tampered_releases"
+tampered_archive="$tampered_releases/v$tampered_version/devloop-$tampered_version.tar.gz"
+printf '%064d  %s\n' 0 "devloop-$tampered_version.tar.gz" > "$tampered_archive.sha256"
+tampered_home="$work/tampered-home"
+if tampered_output="$(
+  HOME="$tampered_home" PATH="/usr/bin:/bin:/usr/sbin:/sbin" bash "$REMOTE_INSTALLER" \
+    --yes \
+    --version "$tampered_version" \
+    --release-base-url "file://$tampered_releases" \
+    2>&1
+)"; then
+  printf '%s\n' "$tampered_output" >&2
+  fail "remote installer accepted checksum mismatch"
+fi
+contains "$tampered_output" "checksum mismatch" "remote checksum mismatch"
+[[ ! -e "$tampered_home/.local/share/devloop/$tampered_version" ]] || fail "checksum mismatch created install dir"
+[[ ! -e "$tampered_home/.local/bin/devloop" ]] || fail "checksum mismatch created devloop symlink"
+ok "remote installer rejects checksum mismatch"
+
+remote_tool_bin="$work/remote-tool-bin"
+mkdir -p "$remote_tool_bin"
+for tool in gum fzf codex claude; do
+  printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$remote_tool_bin/$tool"
+  chmod +x "$remote_tool_bin/$tool"
+done
+remote_path="$remote_tool_bin:/usr/bin:/bin:/usr/sbin:/sbin"
+remote_home="$work/remote-home"
+remote_install_output="$(
+  HOME="$remote_home" PATH="$remote_path" bash "$REMOTE_INSTALLER" \
+    --yes \
+    --version "$remote_version" \
+    --release-base-url "$remote_release_base" \
+    2>&1
+)"
+remote_default_root="$remote_home/.local/share/devloop"
+remote_default_bin="$remote_home/.local/bin"
+[[ -d "$remote_default_root/$remote_version" ]] || fail "remote installer did not create versioned install dir"
+[[ -L "$remote_default_bin/devloop" ]] || fail "remote installer did not create devloop symlink"
+equals "$(readlink "$remote_default_bin/devloop")" "$remote_default_root/$remote_version/devloop" "remote installer symlink target"
+equals "$("$remote_default_bin/devloop" --version)" "devloop $remote_version" "remote installed version"
+contains "$remote_install_output" "verified checksum" "remote install checksum"
+contains "$remote_install_output" "$remote_default_bin is not on PATH" "remote install PATH guidance"
+contains "$remote_install_output" "export PATH=\"$remote_default_bin:\$PATH\"" "remote install PATH guidance"
+contains "$remote_install_output" "[ok] gum:" "remote install UI check"
+contains "$remote_install_output" "[ok] codex:" "remote install agent check"
+[[ -f "$remote_home/.agents/skills/devloop-spec/SKILL.md" ]] || fail "remote installer did not install Codex spec skill"
+[[ -f "$remote_home/.agents/skills/devloop-review/.devloop-checksum" ]] || fail "remote installer did not write Codex skill checksum"
+[[ -f "$remote_home/.claude/skills/devloop-spec/SKILL.md" ]] || fail "remote installer did not install Claude spec skill"
+[[ -f "$remote_home/.claude/skills/devloop-review/.devloop-checksum" ]] || fail "remote installer did not write Claude skill checksum"
+ok "remote installer successful install"
+
+printf '%s\n' "user edit" >> "$remote_home/.agents/skills/devloop-review/SKILL.md"
+remote_preserve_output="$(
+  HOME="$remote_home" PATH="$remote_path" bash "$REMOTE_INSTALLER" \
+    --yes \
+    --version "$remote_version" \
+    --release-base-url "$remote_release_base" \
+    2>&1
+)"
+contains "$remote_preserve_output" "skipping modified skill" "remote installer modified skill guard"
+contains "$(cat "$remote_home/.agents/skills/devloop-review/SKILL.md")" "user edit" "remote installer modified skill preserved"
+ok "remote installer skill preservation"
+
+remote_no_skills_home="$work/remote-no-skills-home"
+remote_no_skills_output="$(
+  HOME="$remote_no_skills_home" PATH="$remote_path" bash "$REMOTE_INSTALLER" \
+    --yes \
+    --no-skills \
+    --version "$remote_version" \
+    --release-base-url "$remote_release_base" \
+    2>&1
+)"
+contains "$remote_no_skills_output" "skipping skill installation" "remote no-skills"
+contains "$remote_no_skills_output" "devloop doctor will require skill installation before agent loops are ready." "remote no-skills"
+[[ ! -e "$remote_no_skills_home/.agents/skills/devloop-spec" ]] || fail "remote no-skills installed Codex skill"
+[[ ! -e "$remote_no_skills_home/.claude/skills/devloop-review" ]] || fail "remote no-skills installed Claude skill"
+ok "remote installer no-skills"
+
 bin_dir="$work/bin"
 install_home="$work/install-home"
 tool_bin="$work/tool-bin"
@@ -530,7 +717,7 @@ done
 BREW
 chmod +x "$tool_bin/brew"
 install_path="$tool_bin:/usr/bin:/bin:/usr/sbin:/sbin"
-DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-test.out
+DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$SCRIPTS_DIR/install.sh" >/tmp/devloop-install-test.out
 [[ -x "$REPO_ROOT/devloop" ]] || fail "devloop is not executable"
 [[ -L "$bin_dir/devloop" ]] || fail "installer did not create symlink"
 contains "$(cat /tmp/devloop-install-test.out)" "gh auth login" "installer optional gh auth"
@@ -548,11 +735,11 @@ contains "$(cat /tmp/devloop-help-test.out)" "Spec-driven code and review loop."
 ok "installer"
 
 printf '%s\n' "user edit" >> "$install_home/.agents/skills/devloop-review/SKILL.md"
-DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-skip.out 2>&1
+DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$SCRIPTS_DIR/install.sh" >/tmp/devloop-install-skip.out 2>&1
 contains "$(cat /tmp/devloop-install-skip.out)" "skipping modified skill" "installer modified skill guard"
 contains "$(cat /tmp/devloop-install-skip.out)" "try: devloop doctor" "installer guidance after skill skip"
 contains "$(cat "$install_home/.agents/skills/devloop-review/SKILL.md")" "user edit" "installer modified skill preserved"
-DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/install.sh" >/tmp/devloop-install-force.out
+DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$SCRIPTS_DIR/install.sh" >/tmp/devloop-install-force.out
 if grep -q "user edit" "$install_home/.agents/skills/devloop-review/SKILL.md"; then fail "installer force did not restore skill"; fi
 ok "installer skill updates"
 
diff --git a/scripts/install.remote.sh b/scripts/install.remote.sh
new file mode 100755
index 0000000..7bb3a2e
--- /dev/null
+++ b/scripts/install.remote.sh
@@ -0,0 +1,359 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+GITHUB_REPO="${DEVLOOP_GITHUB_REPO:-satyaborg/devloop}"
+RELEASE_BASE_URL="${DEVLOOP_RELEASE_BASE_URL:-https://github.com/$GITHUB_REPO/releases/download}"
+GITHUB_API_URL="${DEVLOOP_GITHUB_API_URL:-https://api.github.com/repos/$GITHUB_REPO/releases/latest}"
+INSTALL_ROOT="${DEVLOOP_INSTALL_DIR:-$HOME/.local/share/devloop}"
+BIN_DIR="${DEVLOOP_BIN_DIR:-$HOME/.local/bin}"
+VERSION=""
+YES=false
+NO_SKILLS=false
+DRY_RUN=false
+
+usage() {
+  cat <<'EOF'
+usage: install.remote.sh [options]
+
+Primary install:
+  curl -fsSL https://devloop.sh/install | bash
+
+Options:
+  --yes                 Run without optional prompts.
+  --version <version>   Install a specific tagged version, for example 0.2.0.
+  --no-skills           Install only the devloop CLI.
+  --dry-run             Print planned actions without changing files.
+  --install-dir <dir>   Versioned install root. Default: ~/.local/share/devloop.
+  --bin-dir <dir>       Directory for the devloop symlink. Default: ~/.local/bin.
+  --release-base-url <url>
+                        Release asset base URL. Default: GitHub releases.
+  -h, --help            Show this help.
+EOF
+}
+
+fail() {
+  printf 'error: %s\n' "$*" >&2
+  exit 1
+}
+
+info() {
+  printf '%s\n' "$*"
+}
+
+version_tag() {
+  printf 'v%s\n' "$1"
+}
+
+asset_name() {
+  printf 'devloop-%s.tar.gz\n' "$1"
+}
+
+artifact_url() {
+  local version="$1"
+  printf '%s/%s/%s\n' "$RELEASE_BASE_URL" "$(version_tag "$version")" "$(asset_name "$version")"
+}
+
+checksum_url() {
+  local version="$1"
+  printf '%s.sha256\n' "$(artifact_url "$version")"
+}
+
+download_file() {
+  local url="$1"
+  local dest="$2"
+  local source_path
+
+  case "$url" in
+    file://*)
+      source_path="${url#file://}"
+      cp "$source_path" "$dest"
+      ;;
+    *)
+      if ! command -v curl >/dev/null 2>&1; then
+        fail "missing curl; install curl or download the release archive manually"
+      fi
+      curl -fsSL "$url" -o "$dest"
+      ;;
+  esac
+}
+
+checksum_file() {
+  local file="$1"
+  if command -v shasum >/dev/null 2>&1; then
+    shasum -a 256 "$file" | awk '{print $1}'
+  elif command -v sha256sum >/dev/null 2>&1; then
+    sha256sum "$file" | awk '{print $1}'
+  else
+    fail "missing sha256 tool; install shasum or sha256sum"
+  fi
+}
+
+resolve_latest_version() {
+  local response version source_path
+  case "$GITHUB_API_URL" in
+    file://*)
+      source_path="${GITHUB_API_URL#file://}"
+      [ -f "$source_path" ] || fail "missing latest release fixture: $source_path"
+      response="$(<"$source_path")"
+      ;;
+    *)
+      if ! command -v curl >/dev/null 2>&1; then
+        fail "missing curl; pass --version <version> or install curl"
+      fi
+      response="$(curl -fsSL "$GITHUB_API_URL")" || fail "failed to resolve latest Devloop release"
+      ;;
+  esac
+  version="$(printf '%s\n' "$response" | sed -nE 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v?([^"]+)".*/\1/p' | head -n 1)"
+  if [ -z "$version" ]; then
+    fail "latest release response did not include tag_name"
+  fi
+  printf '%s\n' "$version"
+}
+
+normalize_version() {
+  local version="$1"
+  version="${version#v}"
+  if ! printf '%s\n' "$version" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*))?(\+([0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*))?$'; then
+    fail "invalid version: $1"
+  fi
+  printf '%s\n' "$version"
+}
+
+parse_args() {
+  while [ "$#" -gt 0 ]; do
+    case "$1" in
+      --yes) YES=true ;;
+      --no-skills) NO_SKILLS=true ;;
+      --dry-run) DRY_RUN=true ;;
+      --version)
+        [ "$#" -ge 2 ] || fail "--version requires a value"
+        VERSION="$2"
+        shift
+        ;;
+      --install-dir)
+        [ "$#" -ge 2 ] || fail "--install-dir requires a value"
+        INSTALL_ROOT="$2"
+        shift
+        ;;
+      --bin-dir)
+        [ "$#" -ge 2 ] || fail "--bin-dir requires a value"
+        BIN_DIR="$2"
+        shift
+        ;;
+      --release-base-url)
+        [ "$#" -ge 2 ] || fail "--release-base-url requires a value"
+        RELEASE_BASE_URL="$2"
+        shift
+        ;;
+      -h|--help)
+        usage
+        exit 0
+        ;;
+      --*)
+        fail "unknown option: $1"
+        ;;
+      *)
+        fail "unexpected argument: $1"
+        ;;
+    esac
+    shift
+  done
+}
+
+missing_commands() {
+  local missing=()
+  local command_name
+  for command_name in "$@"; do
+    if ! command -v "$command_name" >/dev/null 2>&1; then
+      missing+=("$command_name")
+    fi
+  done
+  if [ "${#missing[@]}" -eq 0 ]; then
+    printf '\n'
+    return 0
+  fi
+  printf '%s\n' "${missing[*]}"
+}
+
+check_ui_tools() {
+  local missing_text="$1"
+  local missing_items=()
+  local reply
+  if [ -z "$missing_text" ]; then
+    info "[ok] gum: $(command -v gum)"
+    info "[ok] fzf: $(command -v fzf)"
+    return 0
+  fi
+
+  read -r -a missing_items <<< "$missing_text"
+  info "missing UI tools: $missing_text"
+  if command -v brew >/dev/null 2>&1; then
+    if [ "$YES" = false ] && [ -t 0 ]; then
+      printf 'Install missing UI tools with Homebrew now? [y/N] '
+      read -r reply
+      case "$reply" in
+        y|Y|yes|YES)
+          brew install "${missing_items[@]}"
+          ;;
+        *)
+          info "install with: brew install $missing_text"
+          return 0
+          ;;
+      esac
+    else
+      info "install with: brew install $missing_text"
+      return 0
+    fi
+  else
+    info "install with: brew install $missing_text"
+    return 0
+  fi
+
+  missing_text="$(missing_commands gum fzf)"
+  if [ -n "$missing_text" ]; then
+    info "still missing UI tools: $missing_text"
+  else
+    info "[ok] gum: $(command -v gum)"
+    info "[ok] fzf: $(command -v fzf)"
+  fi
+}
+
+check_agent_tools() {
+  local agent_missing_text="$1"
+  if [ -z "$agent_missing_text" ]; then
+    info "[ok] codex: $(command -v codex)"
+    info "[ok] claude: $(command -v claude)"
+    return 0
+  fi
+
+  info "missing agent CLIs: $agent_missing_text"
+  info "Devloop does not install codex or claude automatically."
+}
+
+print_path_guidance() {
+  case ":${PATH:-}:" in
+    *":$BIN_DIR:"*) ;;
+    *)
+      info ""
+      info "$BIN_DIR is not on PATH. Add this to your shell profile:"
+      info "export PATH=\"$BIN_DIR:\$PATH\""
+      ;;
+  esac
+}
+
+verify_archive() {
+  local archive="$1"
+  local checksum="$2"
+  local expected actual
+  expected="$(awk '{print $1; exit}' "$checksum")"
+  [ -n "$expected" ] || fail "empty checksum file: $checksum"
+  actual="$(checksum_file "$archive")"
+  [ "$expected" = "$actual" ] || fail "checksum mismatch for $archive"
+  info "verified checksum"
+}
+
+find_extracted_root() {
+  local extract_dir="$1"
+  local found
+  found="$(find "$extract_dir" -mindepth 1 -maxdepth 1 -type d | head -n 1)"
+  [ -n "$found" ] || fail "release archive did not contain an install directory"
+  [ -f "$found/devloop" ] || fail "release archive missing devloop executable"
+  [ -f "$found/scripts/skill_helpers.sh" ] || fail "release archive missing scripts/skill_helpers.sh"
+  [ -d "$found/skills" ] || fail "release archive missing bundled skills"
+  printf '%s\n' "$found"
+}
+
+install_archive() {
+  local version="$1"
+  local archive="$2"
+  local target="$INSTALL_ROOT/$version"
+  local tmp extract_dir extracted_root
+
+  tmp="$(mktemp -d "${TMPDIR:-/tmp}/devloop-install.XXXXXX")"
+  extract_dir="$tmp/extract"
+  mkdir -p "$extract_dir"
+  tar -xzf "$archive" -C "$extract_dir"
+  extracted_root="$(find_extracted_root "$extract_dir")"
+
+  mkdir -p "$INSTALL_ROOT" "$BIN_DIR"
+  rm -rf "$target.tmp"
+  cp -R "$extracted_root" "$target.tmp"
+  chmod +x "$target.tmp/devloop"
+  rm -rf "$target"
+  mv "$target.tmp" "$target"
+  ln -sfn "$target/devloop" "$BIN_DIR/devloop"
+  rm -rf "$tmp"
+
+  info "installed devloop $version -> $target"
+  info "linked $BIN_DIR/devloop -> $target/devloop"
+}
+
+install_skills() {
+  local root="$1"
+  if [ "$NO_SKILLS" = true ]; then
+    info "skipping skill installation"
+    info "devloop doctor will require skill installation before agent loops are ready."
+    return 0
+  fi
+
+  DEVLOOP_SKILL_INSTALL=copy
+  export DEVLOOP_SKILL_INSTALL
+  # shellcheck source=/dev/null
+  source "$root/scripts/skill_helpers.sh"
+  devloop_install_skills "$root"
+}
+
+dry_run() {
+  local version="$1"
+  info "dry run: no files will be changed"
+  info "version: $version"
+  info "download: $(artifact_url "$version")"
+  info "verify: $(checksum_url "$version")"
+  info "install: $INSTALL_ROOT/$version"
+  info "link: $BIN_DIR/devloop -> $INSTALL_ROOT/$version/devloop"
+  if [ "$NO_SKILLS" = true ]; then
+    info "skills: skipped"
+    info "devloop doctor will require skill installation before agent loops are ready."
+  else
+    info "skills: $HOME/.agents/skills, $HOME/.claude/skills"
+  fi
+}
+
+main() {
+  local version ui_missing agent_missing tmp archive checksum installed_root
+  parse_args "$@"
+
+  if [ -z "$VERSION" ]; then
+    VERSION="$(resolve_latest_version)"
+  fi
+  version="$(normalize_version "$VERSION")"
+  ui_missing="$(missing_commands gum fzf)"
+  agent_missing="$(missing_commands codex claude)"
+
+  if [ "$DRY_RUN" = true ]; then
+    dry_run "$version"
+    check_ui_tools "$ui_missing"
+    check_agent_tools "$agent_missing"
+    print_path_guidance
+    return 0
+  fi
+
+  tmp="$(mktemp -d "${TMPDIR:-/tmp}/devloop-download.XXXXXX")"
+  archive="$tmp/$(asset_name "$version")"
+  checksum="$archive.sha256"
+  download_file "$(artifact_url "$version")" "$archive"
+  download_file "$(checksum_url "$version")" "$checksum"
+  verify_archive "$archive" "$checksum"
+  install_archive "$version" "$archive"
+  rm -rf "$tmp"
+
+  installed_root="$INSTALL_ROOT/$version"
+  install_skills "$installed_root"
+  check_ui_tools "$ui_missing"
+  check_agent_tools "$agent_missing"
+  print_path_guidance
+  info ""
+  info "try: devloop doctor"
+}
+
+main "$@"
diff --git a/install.sh b/scripts/install.sh
similarity index 88%
rename from install.sh
rename to scripts/install.sh
index 520f5d7..85bf18d 100755
--- a/install.sh
+++ b/scripts/install.sh
@@ -11,8 +11,9 @@ while [ -L "$SCRIPT_PATH" ]; do
   esac
 done
 
-ROOT="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)"
-source "$ROOT/skill_helpers.sh"
+SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)"
+ROOT="$(cd -P "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd)"
+source "$ROOT/scripts/skill_helpers.sh"
 
 BIN_DIR="${DEVLOOP_BIN_DIR:-$HOME/.local/bin}"
 TARGET="$BIN_DIR/devloop"
@@ -37,7 +38,7 @@ install_required_ui_tools() {
 
   if ! command -v brew >/dev/null 2>&1; then
     echo "missing required UI tools: ${missing[*]}" >&2
-    echo "install Homebrew, then rerun ./install.sh" >&2
+    echo "install Homebrew, then rerun ./scripts/install.sh" >&2
     return 1
   fi
 
diff --git a/release.sh b/scripts/release.sh
similarity index 69%
rename from release.sh
rename to scripts/release.sh
index b18a65c..7ea4ae0 100755
--- a/release.sh
+++ b/scripts/release.sh
@@ -11,19 +11,22 @@ while [ -L "$SCRIPT_PATH" ]; do
   esac
 done
 
-ROOT="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)"
+SCRIPT_DIR="$(cd -P "$(dirname "$SCRIPT_PATH")" >/dev/null 2>&1 && pwd)"
+ROOT="$(cd -P "$SCRIPT_DIR/.." >/dev/null 2>&1 && pwd)"
+RELEASE_ARCHIVE=""
+RELEASE_CHECKSUM=""
 
 release_usage() {
   cat <<'EOF'
-usage: ./release.sh <patch|minor|major> [--dry-run] [--publish] [--push]
+usage: ./scripts/release.sh <patch|minor|major> [--dry-run] [--publish] [--push]
 
 Bumps VERSION, creates a release commit, and creates an annotated tag.
 Use --publish to push the commit and tag, then create a GitHub Release.
 
 Examples:
-  ./release.sh patch --dry-run
-  ./release.sh minor --publish
-  ./release.sh major --push
+  ./scripts/release.sh patch --dry-run
+  ./scripts/release.sh minor --publish
+  ./scripts/release.sh major --push
 EOF
 }
 
@@ -107,6 +110,45 @@ release_current_branch() {
   git -C "$ROOT" branch --show-current
 }
 
+release_checksum_file() {
+  local file="$1"
+  if command -v shasum >/dev/null 2>&1; then
+    shasum -a 256 "$file" | awk '{print $1}'
+  elif command -v sha256sum >/dev/null 2>&1; then
+    sha256sum "$file" | awk '{print $1}'
+  else
+    printf '%s\n' "missing shasum or sha256sum" >&2
+    return 1
+  fi
+}
+
+release_create_artifacts() {
+  local version="$1"
+  local out_dir="$2"
+  local name="devloop-$version"
+  local staging="$out_dir/$name"
+
+  rm -rf "$staging"
+  mkdir -p "$staging/scripts"
+  cp "$ROOT/devloop" "$staging/devloop"
+  cp "$ROOT/scripts/install.sh" "$staging/scripts/install.sh"
+  cp "$ROOT/scripts/install.remote.sh" "$staging/scripts/install.remote.sh"
+  cp "$ROOT/scripts/devloop_test.sh" "$staging/scripts/devloop_test.sh"
+  cp "$ROOT/scripts/release.sh" "$staging/scripts/release.sh"
+  cp "$ROOT/scripts/skill_helpers.sh" "$staging/scripts/skill_helpers.sh"
+  cp "$ROOT/README.md" "$staging/README.md"
+  cp "$ROOT/LICENSE" "$staging/LICENSE"
+  cp "$ROOT/CHANGELOG.md" "$staging/CHANGELOG.md"
+  cp "$ROOT/VERSION" "$staging/VERSION"
+  cp -R "$ROOT/skills" "$staging/skills"
+
+  RELEASE_ARCHIVE="$out_dir/$name.tar.gz"
+  RELEASE_CHECKSUM="$RELEASE_ARCHIVE.sha256"
+  tar -C "$out_dir" -czf "$RELEASE_ARCHIVE" "$name"
+  printf '%s  %s\n' "$(release_checksum_file "$RELEASE_ARCHIVE")" "$name.tar.gz" > "$RELEASE_CHECKSUM"
+  rm -rf "$staging"
+}
+
 release_assert_push_branch() {
   local branch
   branch="$(release_current_branch)"
@@ -127,6 +169,7 @@ release_main() {
   local publish=false
   local push=false
   local tag branch
+  local artifact_dir
 
   while [ "$#" -gt 0 ]; do
     case "$1" in
@@ -183,12 +226,15 @@ release_main() {
     if [ -n "$(git -C "$ROOT" status --porcelain)" ]; then
       printf '%s\n' "note: actual release requires a clean working tree"
     fi
-    printf '%s\n' "would run bash tests/devloop_test.sh"
+    printf '%s\n' "would run bash scripts/devloop_test.sh"
     printf '%s\n' "would update VERSION and CHANGELOG.md"
     printf 'would commit: chore: release %s\n' "$version"
     printf 'would tag: %s\n' "$tag"
     if [ "$publish" = true ] || [ "$push" = true ]; then printf '%s\n' "would push branch and tag"; fi
-    if [ "$publish" = true ]; then printf 'would create GitHub release: gh release create %s --verify-tag --generate-notes\n' "$tag"; fi
+    if [ "$publish" = true ]; then
+      printf 'would build release assets: devloop-%s.tar.gz and devloop-%s.tar.gz.sha256\n' "$version" "$version"
+      printf 'would create GitHub release: gh release create %s --verify-tag --generate-notes devloop-%s.tar.gz devloop-%s.tar.gz.sha256\n' "$tag" "$version" "$version"
+    fi
     return 0
   fi
 
@@ -198,7 +244,7 @@ release_main() {
   release_assert_clean_tree
   if [ "$publish" = true ] || [ "$push" = true ]; then release_assert_push_branch; fi
 
-  bash "$ROOT/tests/devloop_test.sh"
+  bash "$ROOT/scripts/devloop_test.sh"
   printf '%s\n' "$version" > "$ROOT/VERSION"
   git-cliff --config "$ROOT/cliff.toml" --workdir "$ROOT" --tag "$tag" --output "$ROOT/CHANGELOG.md"
   git -C "$ROOT" add VERSION CHANGELOG.md
@@ -212,7 +258,10 @@ release_main() {
   fi
 
   if [ "$publish" = true ]; then
-    gh release create "$tag" --verify-tag --generate-notes
+    artifact_dir="$(mktemp -d "${TMPDIR:-/tmp}/devloop-release.XXXXXX")"
+    release_create_artifacts "$version" "$artifact_dir"
+    gh release create "$tag" --verify-tag --generate-notes "$RELEASE_ARCHIVE" "$RELEASE_CHECKSUM"
+    rm -rf "$artifact_dir"
   fi
 
   printf 'released %s\n' "$tag"
diff --git a/skill_helpers.sh b/scripts/skill_helpers.sh
similarity index 99%
rename from skill_helpers.sh
rename to scripts/skill_helpers.sh
index 7cd74fa..6dcd038 100644
--- a/skill_helpers.sh
+++ b/scripts/skill_helpers.sh
@@ -213,7 +213,7 @@ devloop_doctor_skills_in_dir() {
     }
 
     if [ "$bundled" != "$installed" ]; then
-      printf '[fail] stale skill: %s (run ./install.sh)\n' "$dest" >&2
+      printf '[fail] stale skill: %s (run ./scripts/install.sh)\n' "$dest" >&2
       status=1
       continue
     fi
