diff --git a/README.md b/README.md
index f9645ac..17328d5 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,16 @@ devloop spec
 devloop spec --agent claude --output .specs/chat-retry.md notes.md
 ```
 
+By default, generated specs are written under `.specs/`. To change that for a repo, open `devloop`, choose `Settings`, and set the default spec directory. The setting is stored in `.devloop/config` and can be repo-relative or absolute:
+
+```ini
+spec_dir=/Users/satya/Projects/specs
+```
+
+The Settings menu expands `~/...` before saving. Absolute paths are machine-local, so avoid committing them as shared project config.
+
+The interactive spec picker searches the configured directory, `.specs/`, and `.devloop/specs/`.
+
 Strict mode is on by default. It requires acceptance criteria and only accepts
 reviews that pass both the spec gate and engineering quality gate:
 
@@ -99,10 +109,11 @@ devloop [options] <spec.md> [max=5]
 
 When stdout is a terminal, running `devloop` without arguments opens a menu:
 
-- `Run a spec`: pick a `.specs/*.md` file, review run settings, then start.
+- `Run a spec`: pick a spec from the configured spec directory, `.specs/`, or `.devloop/specs/`.
 - `Create a spec`: choose the spec agent and provide source context.
 - `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`: edit repo-local settings such as the default spec directory.
 - `Doctor`: verify required commands, optional UI tools, and installed skills.
 
 `gum` powers prompts, confirmations, status output, paging, and setup screens. `fzf` powers searchable pickers for specs, tracks, and reports.
diff --git a/devloop b/devloop
index 10afbcd..94279e9 100755
--- a/devloop
+++ b/devloop
@@ -170,6 +170,139 @@ has_arg() {
   return 1
 }
 
+trim_string() {
+  printf '%s\n' "$1" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//'
+}
+
+devloop_config_file() {
+  printf '%s\n' ".devloop/config"
+}
+
+devloop_config_value() {
+  local key="$1"
+  local file line value
+  file="$(devloop_config_file)"
+  [ -f "$file" ] || return 1
+  while IFS= read -r line; do
+    value="$(printf '%s\n' "$line" | sed -nE "s/^[[:space:]]*$key[[:space:]]*=[[:space:]]*(.*)$/\1/p")"
+    [ -n "$value" ] || continue
+    value="${value%%#*}"
+    value="$(trim_string "$value")"
+    case "$value" in
+      \"*\") value="${value#\"}"; value="${value%\"}" ;;
+    esac
+    printf '%s\n' "$value"
+  done < "$file" | tail -n 1
+}
+
+normalize_spec_dir() {
+  local dir="$1"
+  dir="$(trim_string "$dir")"
+  case "$dir" in
+    ./*)
+      while [ "${dir#./}" != "$dir" ]; do dir="${dir#./}"; done
+      ;;
+  esac
+  while [ "$dir" != "/" ] && [ "$dir" != "." ] && [ "${dir%/}" != "$dir" ]; do dir="${dir%/}"; done
+  case "$dir" in
+    ""|"."|".."|"/"|\~*|../*|*/../*|*/..) return 1 ;;
+  esac
+  printf '%s\n' "$dir"
+}
+
+expand_spec_dir_input() {
+  local dir="$1"
+  dir="$(trim_string "$dir")"
+  case "$dir" in
+    "~")
+      if [ -z "${HOME:-}" ]; then return 1; fi
+      printf '%s\n' "$HOME"
+      ;;
+    "~/"*)
+      if [ -z "${HOME:-}" ]; then return 1; fi
+      printf '%s/%s\n' "${HOME%/}" "${dir#\~/}"
+      ;;
+    *)
+      printf '%s\n' "$dir"
+      ;;
+  esac
+}
+
+devloop_spec_dir() {
+  local value normalized
+  value="$(devloop_config_value spec_dir || true)"
+  if normalized="$(normalize_spec_dir "$value")"; then
+    printf '%s\n' "$normalized"
+  else
+    printf '%s\n' ".specs"
+  fi
+}
+
+spec_search_dirs() {
+  local configured dir seen
+  configured="$(devloop_spec_dir)"
+  seen=""
+  for dir in "$configured" ".specs" ".devloop/specs"; do
+    [ -n "$dir" ] || continue
+    case "$seen" in
+      *"|$dir|"*) continue ;;
+    esac
+    seen="$seen|$dir|"
+    printf '%s\n' "$dir"
+  done
+}
+
+spec_search_label() {
+  local label="" dir
+  while IFS= read -r dir; do
+    if [ -z "$label" ]; then label="$dir"; else label="$label, $dir"; fi
+  done <<EOF
+$(spec_search_dirs)
+EOF
+  printf '%s\n' "$label"
+}
+
+list_spec_files() {
+  local dir
+  spec_search_dirs |
+    while IFS= read -r dir; do
+      if [ -d "$dir" ]; then
+        find "$dir" -type f -name '*.md'
+      fi
+    done | LC_ALL=C sort
+}
+
+spec_dir_status() {
+  local dir="$1"
+  if [ -d "$dir" ]; then
+    printf '%s\n' "exists"
+  else
+    printf '%s\n' "missing"
+  fi
+}
+
+write_config_spec_dir() {
+  local dir="$1"
+  local expanded normalized file tmp
+  expanded="$(expand_spec_dir_input "$dir")" || {
+    printf '%s\n' "spec directory could not be expanded" >&2
+    return 2
+  }
+  normalized="$(normalize_spec_dir "$expanded")" || {
+    printf '%s\n' "spec directory must be absolute, or relative without '..'" >&2
+    return 2
+  }
+  file="$(devloop_config_file)"
+  mkdir -p "$(dirname "$file")" "$normalized" || return 1
+  tmp="$(mktemp "${TMPDIR:-/tmp}/devloop-config.XXXXXX")"
+  if [ -f "$file" ]; then
+    sed -E '/^[[:space:]]*spec_dir[[:space:]]*=/d' "$file" > "$tmp"
+  fi
+  printf 'spec_dir=%s\n' "$normalized" >> "$tmp"
+  mv "$tmp" "$file"
+  printf '%s\n' "$normalized"
+}
+
 ui_has_gum() {
   [ "$USE_TUI" = true ] && command -v gum >/dev/null 2>&1
 }
@@ -463,6 +596,7 @@ interactive_menu() {
       "Create a spec" \
       "Continue a run" \
       "Open reports" \
+      "Settings" \
       "Doctor" \
       "Quit")" || return 130
     case "$choice" in
@@ -470,6 +604,7 @@ interactive_menu() {
       "Create a spec") interactive_create_spec; return $? ;;
       "Continue a run") continue_command; return $? ;;
       "Open reports") reports_command; return $? ;;
+      "Settings") interactive_settings || return $? ;;
       "Doctor") devloop_doctor "$ROOT_DIR"; return $? ;;
       "Quit") return 0 ;;
     esac
@@ -479,7 +614,7 @@ interactive_menu() {
 interactive_run_spec() {
   local spec
   if ! spec="$(select_spec_file)"; then
-    printf '%s\n' "no specs found under .specs" >&2
+    printf 'no specs found under %s\n' "$(spec_search_label)" >&2
     if ui_confirm "Create a spec now?"; then
       interactive_create_spec
       return $?
@@ -500,6 +635,33 @@ interactive_create_spec() {
   fi
 }
 
+interactive_settings() {
+  local choice value saved spec_dir
+  while true; do
+    spec_dir="$(devloop_spec_dir)"
+    ui_header "Settings" "Repo-local devloop config"
+    ui_print_key_values \
+      "config" "$(devloop_config_file)" \
+      "default spec dir" "$spec_dir" \
+      "spec dir status" "$(spec_dir_status "$spec_dir")" \
+      "searched spec dirs" "$(spec_search_label)"
+    choice="$(ui_choose "Edit settings" \
+      "Change default spec directory" \
+      "Back")" || return 130
+    case "$choice" in
+      "Change default spec directory")
+        value="$(ui_input "Default spec directory" "$spec_dir")" || return 130
+        if saved="$(write_config_spec_dir "$value")"; then
+          printf 'settings saved: spec_dir=%s\n' "$saved" >&2
+        else
+          printf '%s\n' "settings not saved" >&2
+        fi
+        ;;
+      "Back") return 0 ;;
+    esac
+  done
+}
+
 interactive_run_setup() {
   local spec="$1"
   local report_format="html"
@@ -579,9 +741,7 @@ interactive_run_setup() {
 select_spec_file() {
   local list selected
   list="$(mktemp "${TMPDIR:-/tmp}/devloop-specs.XXXXXX")"
-  if [ -d ".specs" ]; then
-    find ".specs" -type f -name '*.md' | LC_ALL=C sort > "$list"
-  fi
+  list_spec_files > "$list"
   selected="$(ui_pick_from_file "$list" "Select spec")"
   local code=$?
   rm -f "$list"
@@ -2345,10 +2505,11 @@ spec_command() {
     skill-path) printf '%s\n' "$skill"; return ;;
   esac
 
-  local context today prompt cmd_args=()
+  local context today prompt spec_dir cmd_args=()
   context="$(resolve_spec_context "${context_items[@]}")"
   today="$(date +%F)"
-  prompt="$(spec_prompt "$context" "$output" "$skill" "$today")"
+  spec_dir="$(devloop_spec_dir)"
+  prompt="$(spec_prompt "$context" "$output" "$skill" "$today" "$spec_dir")"
   if [ "$USE_TUI" = true ]; then event_step "spec" "generate spec with $agent"; fi
   if [ "$agent" = "codex" ]; then
     run_with_prompt "$PWD" "" "" "$prompt" codex exec "${CODEX_MODEL_ARGS[@]}" "${CODEX_REASONING_ARGS[@]}" -s read-only -C "$PWD" -
@@ -2407,11 +2568,12 @@ spec_prompt() {
   local output="$2"
   local skill="$3"
   local today="$4"
+  local spec_dir="$5"
   local output_line
   if [ -n "$output" ]; then
     output_line="Output path: $output"
   else
-    output_line="Output path: choose a .specs/YYYY-MM-DD-<slug>.md path if you write a file; otherwise return markdown on stdout."
+    output_line="Output path: choose a $spec_dir/YYYY-MM-DD-<slug>.md path if you write a file; otherwise return markdown on stdout."
   fi
   cat <<EOF
 Use the installed devloop-spec skill to produce one implementation spec.
@@ -2458,7 +2620,7 @@ generated_spec_path() {
   local output="$2"
   local today="$3"
   local force="$4"
-  local title slug file base ext index
+  local title slug file base ext index spec_dir
   if [ -n "$output" ]; then
     absolute_path "$output"
     return
@@ -2466,7 +2628,11 @@ generated_spec_path() {
   title="$(printf '%s\n' "$markdown" | sed -nE 's/^#[[:space:]]+(.+)/\1/p' | head -n 1)"
   slug="$(slugify "${title:-spec}")"
   if [ -z "$slug" ]; then slug="spec"; fi
-  file="$PWD/.specs/$today-$slug.md"
+  spec_dir="$(devloop_spec_dir)"
+  case "$spec_dir" in
+    /*) file="$spec_dir/$today-$slug.md" ;;
+    *) file="$PWD/$spec_dir/$today-$slug.md" ;;
+  esac
   if [ "$force" = true ]; then
     printf '%s\n' "$file"
     return
diff --git a/skills/devloop-spec/SKILL.md b/skills/devloop-spec/SKILL.md
index 00cfa65..1012ca8 100644
--- a/skills/devloop-spec/SKILL.md
+++ b/skills/devloop-spec/SKILL.md
@@ -124,4 +124,4 @@ Keep every standard section present, remove leftover placeholders, and list the
 
 ## Output
 
-When a caller provides an output path, write the spec there. Otherwise, write only the markdown spec to stdout or save it under `.specs/YYYY-MM-DD-<slug>.md` in the target repository. Do not wrap the spec in a code fence unless the caller explicitly asks for a fenced snippet.
+When a caller provides an output path, write the spec there. Otherwise, write only the markdown spec to stdout or save it under the caller's requested default spec directory, usually `.specs/YYYY-MM-DD-<slug>.md`. Do not wrap the spec in a code fence unless the caller explicitly asks for a fenced snippet.
diff --git a/tests/devloop_test.sh b/tests/devloop_test.sh
index 4a04b21..220ba81 100755
--- a/tests/devloop_test.sh
+++ b/tests/devloop_test.sh
@@ -189,6 +189,47 @@ equals "$(next_pass_from_track "$branch_repo/.codex/tracks/chat-retry.md")" "4"
 spec_output=$'preface\n---\nstatus: draft\n---\n\n# Generated'
 equals "$(extract_generated_spec "$spec_output")" $'---\nstatus: draft\n---\n\n# Generated' "extract_generated_spec"
 
+config_repo="$work/config-repo"
+mkdir -p "$config_repo/.specs" "$config_repo/.devloop/specs"
+config_repo_real="$(cd "$config_repo" && pwd)"
+printf '%s\n' "# Legacy" > "$config_repo/.specs/legacy.md"
+printf '%s\n' "# Devloop" > "$config_repo/.devloop/specs/devloop.md"
+config_specs="$(cd "$config_repo" && list_spec_files)"
+contains "$config_specs" ".specs/legacy.md" "default spec search"
+contains "$config_specs" ".devloop/specs/devloop.md" "default spec search"
+equals "$(cd "$config_repo" && spec_search_label)" ".specs, .devloop/specs" "spec search label"
+equals "$(cd "$config_repo" && write_config_spec_dir "custom-specs")" "custom-specs" "write config spec dir"
+equals "$(cd "$config_repo" && devloop_spec_dir)" "custom-specs" "configured spec dir"
+[[ -d "$config_repo/custom-specs" ]] || fail "configured spec dir was not created"
+configured_specs="$(cd "$config_repo" && list_spec_files)"
+contains "$configured_specs" ".specs/legacy.md" "configured spec search includes legacy dir"
+contains "$configured_specs" ".devloop/specs/devloop.md" "configured spec search includes devloop dir"
+equals "$(cd "$config_repo" && generated_spec_path "$spec_output" "" "2026-05-29" false)" "$config_repo_real/custom-specs/2026-05-29-generated.md" "configured generated spec path"
+if (cd "$config_repo" && write_config_spec_dir "../bad") >/dev/null 2>&1; then fail "write_config_spec_dir accepted path traversal"; fi
+
+absolute_specs="$work/shared-specs"
+equals "$(cd "$config_repo" && write_config_spec_dir "$absolute_specs")" "$absolute_specs" "write absolute config spec dir"
+equals "$(cd "$config_repo" && devloop_spec_dir)" "$absolute_specs" "absolute configured spec dir"
+[[ -d "$absolute_specs" ]] || fail "absolute configured spec dir was not created"
+printf '%s\n' "# Shared" > "$absolute_specs/shared.md"
+absolute_configured_specs="$(cd "$config_repo" && list_spec_files)"
+contains "$absolute_configured_specs" "$absolute_specs/shared.md" "configured spec search includes absolute dir"
+contains "$absolute_configured_specs" ".specs/legacy.md" "absolute spec search includes legacy dir"
+equals "$(cd "$config_repo" && generated_spec_path "$spec_output" "" "2026-05-29" false)" "$absolute_specs/2026-05-29-generated.md" "absolute generated spec path"
+equals "$(spec_dir_status "$absolute_specs")" "exists" "spec dir status exists"
+equals "$(spec_dir_status "$work/missing-specs")" "missing" "spec dir status missing"
+
+tilde_repo="$work/tilde-repo"
+tilde_home="$work/home"
+mkdir -p "$tilde_repo" "$tilde_home"
+equals "$(cd "$tilde_repo" && HOME="$tilde_home" write_config_spec_dir "~/shared-specs")" "$tilde_home/shared-specs" "tilde input expands when saved"
+equals "$(cat "$tilde_repo/.devloop/config")" "spec_dir=$tilde_home/shared-specs" "tilde input saved as absolute path"
+
+raw_tilde_repo="$work/raw-tilde-repo"
+mkdir -p "$raw_tilde_repo/.devloop"
+printf '%s\n' "spec_dir=~/raw-specs" > "$raw_tilde_repo/.devloop/config"
+equals "$(cd "$raw_tilde_repo" && devloop_spec_dir)" ".specs" "raw tilde config falls back"
+
 session_output=$'unrelated 11111111-1111-4111-8111-111111111111\nTo continue this session, run codex exec resume 22222222-2222-4222-8222-222222222222'
 equals "$(extract_session_id "$session_output")" "22222222-2222-4222-8222-222222222222" "extract_session_id uses session marker"
 
@@ -258,12 +299,15 @@ AGENT
 chmod +x "$agent"
 
 repo="$work/repo"
-mkdir -p "$repo"
+mkdir -p "$repo/.devloop"
+repo_specs="$work/repo-specs"
+printf 'spec_dir=%s\n' "$repo_specs" > "$repo/.devloop/config"
 (
   cd "$repo"
   "$ROOT/devloop" spec --agent "$agent" "Keep devloop as Bash." >/tmp/devloop-spec-test.out
 )
 contains "$(cat /tmp/devloop-spec-test.out)" "spec:" "spec command"
-[[ -f "$repo/.specs/$(date +%F)-shell-migration-spec.md" ]] || fail "spec command did not write dated spec"
+[[ -f "$repo_specs/$(date +%F)-shell-migration-spec.md" ]] || fail "spec command did not write dated spec under absolute configured dir"
 contains "$(cat /tmp/devloop-spec-agent-prompt.txt)" "Keep devloop as Bash." "spec prompt"
+contains "$(cat /tmp/devloop-spec-agent-prompt.txt)" "Output path: choose a $repo_specs/" "spec prompt configured output"
 ok "spec generation"
