diff --git a/README.md b/README.md
index 77169d3..4b23c21 100644
--- a/README.md
+++ b/README.md
@@ -8,8 +8,8 @@ By default, Codex makes the change, Claude Code reviews it, and Codex retries un
 
 ## Install
 
-Prereqs: Bash, git, and the agent CLIs you want to use. The default pairing requires `codex` and `claude`.
-For the full interactive UI, install [`gum`](https://github.com/charmbracelet/gum) and [`fzf`](https://github.com/junegunn/fzf). They are optional; `devloop` falls back to plain terminal output when they are missing.
+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`.
 
 ```sh
 git clone https://github.com/satyaborg/devloop.git
@@ -139,11 +139,11 @@ When stdout is a terminal, running `devloop` without arguments opens a menu:
 - `Continue a run`: pick a prior `.codex/tracks/*.md` and continue in that worktree.
 - `Open reports`: pick a prior report from any registered worktree.
 - `Settings`: view or change the shared spec path and set the run timeout.
-- `Doctor`: verify required commands, optional UI tools, and installed skills.
+- `Doctor`: verify required dependencies and installed skills.
 
 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.
+`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.
 
 ## What Devloop Does
 
diff --git a/devloop b/devloop
index 0510c1f..1923f0d 100755
--- a/devloop
+++ b/devloop
@@ -207,7 +207,7 @@ welcome_tui() {
   gum style --foreground "$UI_ACCENT_COLOR" --bold "Usage"
   printf '  devloop [options] <spec.md> [max=5]\n\n'
   gum style --foreground "$UI_ACCENT_COLOR" --bold "Common commands"
-  printf '  %-42s %s\n' "devloop doctor" "check required and optional tools"
+  printf '  %-42s %s\n' "devloop doctor" "check required tools"
   printf '  %-42s %s\n' "devloop reports" "open previous run reports"
   printf '  %-42s %s\n' "devloop status" "summarize tracked runs"
   printf '  %-42s %s\n' "devloop clean" "show safe cleanup candidates"
diff --git a/install.sh b/install.sh
index 301603b..b596fa4 100755
--- a/install.sh
+++ b/install.sh
@@ -18,6 +18,39 @@ BIN_DIR="${DEVLOOP_BIN_DIR:-$HOME/.local/bin}"
 TARGET="$BIN_DIR/devloop"
 SOURCE="$ROOT/devloop"
 SKILL_STATUS=0
+TOOL_STATUS=0
+
+install_required_ui_tools() {
+  local missing=()
+  local tool
+
+  for tool in gum fzf; do
+    if ! command -v "$tool" >/dev/null 2>&1; then
+      missing+=("$tool")
+    fi
+  done
+
+  if [ "${#missing[@]}" -eq 0 ]; then
+    echo "required UI tools ready"
+    return 0
+  fi
+
+  if ! command -v brew >/dev/null 2>&1; then
+    echo "missing required UI tools: ${missing[*]}" >&2
+    echo "install Homebrew, then rerun ./install.sh" >&2
+    return 1
+  fi
+
+  echo "installing required UI tools: ${missing[*]}"
+  brew install "${missing[@]}"
+
+  for tool in "${missing[@]}"; do
+    if ! command -v "$tool" >/dev/null 2>&1; then
+      echo "failed to install required UI tool: $tool" >&2
+      return 1
+    fi
+  done
+}
 
 if [ ! -f "$SOURCE" ]; then
   echo "missing devloop executable: $SOURCE" >&2
@@ -29,6 +62,7 @@ chmod +x "$SOURCE"
 ln -sfn "$SOURCE" "$TARGET"
 
 echo "installed devloop -> $SOURCE"
+install_required_ui_tools || TOOL_STATUS=$?
 devloop_install_skills "$ROOT" || SKILL_STATUS=$?
 
 case ":${PATH:-}:" in
@@ -42,4 +76,7 @@ esac
 
 echo
 echo "try: devloop doctor"
-exit "$SKILL_STATUS"
+if [ "$TOOL_STATUS" -ne 0 ] || [ "$SKILL_STATUS" -ne 0 ]; then
+  exit 1
+fi
+exit 0
diff --git a/skill_helpers.sh b/skill_helpers.sh
index 6ec51e0..9ffaaf5 100644
--- a/skill_helpers.sh
+++ b/skill_helpers.sh
@@ -156,18 +156,6 @@ devloop_doctor_command() {
   return 1
 }
 
-devloop_doctor_optional_command() {
-  local command="$1"
-  local detail="$2"
-  local resolved
-  resolved="$(command -v "$command" 2>/dev/null || true)"
-  if [ -n "$resolved" ]; then
-    printf '[ok] optional %s: %s (%s)\n' "$command" "$resolved" "$detail"
-    return 0
-  fi
-  printf '[skip] optional %s: %s\n' "$command" "$detail"
-}
-
 devloop_doctor_skills() {
   local root="$1"
   local skills_dir status
@@ -241,14 +229,13 @@ devloop_doctor() {
   local status=0
 
   printf 'devloop doctor\n'
-  printf 'Required\n'
+  printf 'Required dependencies\n'
   devloop_doctor_command devloop || status=1
   devloop_doctor_command git || status=1
   devloop_doctor_command codex || status=1
   devloop_doctor_command claude || status=1
-  printf '\nOptional UI\n'
-  devloop_doctor_optional_command gum "rich prompts, confirmations, status, and paging"
-  devloop_doctor_optional_command fzf "searchable spec, track, and report pickers"
+  devloop_doctor_command gum || status=1
+  devloop_doctor_command fzf || status=1
   printf '\nSkills\n'
   devloop_doctor_skills "$root" || status=1
 
diff --git a/tests/devloop_test.sh b/tests/devloop_test.sh
index 10bf8f1..0446591 100755
--- a/tests/devloop_test.sh
+++ b/tests/devloop_test.sh
@@ -19,6 +19,13 @@ contains() {
   [[ "$haystack" == *"$needle"* ]] || fail "$label missing: $needle"
 }
 
+not_contains() {
+  local haystack="$1"
+  local needle="$2"
+  local label="$3"
+  [[ "$haystack" != *"$needle"* ]] || fail "$label should not contain: $needle"
+}
+
 equals() {
   local actual="$1"
   local expected="$2"
@@ -492,9 +499,31 @@ ok "release helpers"
 
 bin_dir="$work/bin"
 install_home="$work/install-home"
-DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" "$REPO_ROOT/install.sh" >/tmp/devloop-install-test.out
+tool_bin="$work/tool-bin"
+mkdir -p "$tool_bin"
+cat > "$tool_bin/brew" <<'BREW'
+#!/usr/bin/env bash
+set -euo pipefail
+if [ "${1:-}" != "install" ]; then exit 1; fi
+shift
+tool_dir="$(cd "$(dirname "$0")" >/dev/null 2>&1 && pwd)"
+for formula in "$@"; do
+  case "$formula" in
+    gum|fzf)
+      printf '%s\n' '#!/usr/bin/env bash' 'exit 0' > "$tool_dir/$formula"
+      chmod +x "$tool_dir/$formula"
+      ;;
+    *) exit 1 ;;
+  esac
+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
 [[ -x "$REPO_ROOT/devloop" ]] || fail "devloop is not executable"
 [[ -L "$bin_dir/devloop" ]] || fail "installer did not create symlink"
+PATH="$install_path" command -v gum >/dev/null 2>&1 || fail "installer did not make gum available"
+PATH="$install_path" command -v fzf >/dev/null 2>&1 || fail "installer did not make fzf available"
 [[ -f "$install_home/.agents/skills/devloop-spec/SKILL.md" ]] || fail "installer did not install Codex spec skill"
 [[ -f "$install_home/.agents/skills/devloop-spec/references/spec-template.md" ]] || fail "installer did not install Codex spec template reference"
 [[ -f "$install_home/.agents/skills/devloop-review/SKILL.md" ]] || fail "installer did not install Codex review skill"
@@ -507,11 +536,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" "$REPO_ROOT/install.sh" >/tmp/devloop-install-skip.out 2>&1
+DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/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" "$REPO_ROOT/install.sh" >/tmp/devloop-install-force.out
+DEVLOOP_FORCE=1 DEVLOOP_BIN_DIR="$bin_dir" HOME="$install_home" PATH="$install_path" "$REPO_ROOT/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"
 
@@ -520,10 +549,15 @@ mkdir -p "$fake_bin"
 printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/codex"
 printf '#!/usr/bin/env bash\nexit 0\n' > "$fake_bin/claude"
 chmod +x "$fake_bin/codex" "$fake_bin/claude"
-doctor_output="$(HOME="$install_home" PATH="$bin_dir:$fake_bin:$PATH" "$bin_dir/devloop" doctor 2>&1)"
+doctor_output="$(HOME="$install_home" PATH="$bin_dir:$tool_bin:$fake_bin:$PATH" "$bin_dir/devloop" doctor 2>&1)"
 contains "$doctor_output" "devloop doctor: ready" "doctor"
+contains "$doctor_output" "Required dependencies" "doctor"
+contains "$doctor_output" "[ok] codex:" "doctor"
+contains "$doctor_output" "[ok] claude:" "doctor"
 contains "$doctor_output" "[ok] skill devloop-spec" "doctor"
-contains "$doctor_output" "Optional UI" "doctor"
+contains "$doctor_output" "[ok] gum:" "doctor"
+contains "$doctor_output" "[ok] fzf:" "doctor"
+not_contains "$doctor_output" "Optional UI" "doctor"
 contains "$doctor_output" "$install_home/.agents/skills/devloop-spec" "doctor Codex skill"
 contains "$doctor_output" "$install_home/.claude/skills/devloop-spec" "doctor Claude skill"
 ok "doctor"
