refactor(claude): settings.json отдан Claude Code, базовые настройки в settings.local.json
- Зачем:
- Claude Code перезаписывает settings.json при каждом «always allow», что создавало
конфликт с managed-копией из dotfiles при повторном запуске install.sh.
- Что:
- settings.json удалён из репозитория, Claude Code владеет им полностью.
- создан settings.local.json (базовые permissions + statusLine), деплоится через симлинк.
- install.sh: копирование + шаблон заменены на ln -sf с бэкапом и guard для edge cases.
- check.sh: проверка обновлена под симлинк вместо «файл + chmod 444».
- тесты: старый тест копирования заменён на 4 новых (симлинк, бэкап, директория, stow-миграция).
- Проверка:
- bash tests/test_install.sh (15 тестов).
- bash agents/.agents/skills/dotfiles-check/check.sh.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
bdccd889fe
commit
9341853479
6 files changed
+117
-69
No files matched your search
@@ -53,7 +53,7 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
|
|||||||
| `zed` | Конфигурация Zed редактора |
|
| `zed` | Конфигурация Zed редактора |
|
||||||
| `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси |
|
| `vscode` | `server-env-setup` для VS Code Server в WSL: секреты + авто-прокси |
|
||||||
|
|
||||||
`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (копирует `settings.json` с `chmod 444`, создаёт `settings.local.json`) и подключает bash_custom.
|
`install.sh` дополнительно настраивает opencode (симлинки, MCP-конфиг), claude (симлинкает `settings.local.json` из репозитория) и подключает bash_custom.
|
||||||
|
|
||||||
### Windows (chezmoi)
|
### Windows (chezmoi)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
|
|||||||
| Инструмент | Что настраивается |
|
| Инструмент | Что настраивается |
|
||||||
|------------|-------------------|
|
|------------|-------------------|
|
||||||
| **Agents** | Каноничное хранилище скиллов: `~/.agents/skills/<name>/SKILL.md` |
|
| **Agents** | Каноничное хранилище скиллов: `~/.agents/skills/<name>/SKILL.md` |
|
||||||
| **Claude** | `~/.claude/skills` → симлинк на agents; `settings.json` (read-only) + `settings.local.json` (локальный) |
|
| **Claude** | `~/.claude/skills` → симлинк на agents; `settings.local.json` → симлинк на репозиторий (базовые permissions + statusLine); `settings.json` — управляется Claude Code |
|
||||||
| **[Opencode](https://github.com/anomalyco/opencode)** | MCP-серверы (context7, web-reader, playwright и др.), `~/.config/opencode/skills` → симлинк на agents |
|
| **[Opencode](https://github.com/anomalyco/opencode)** | MCP-серверы (context7, web-reader, playwright и др.), `~/.config/opencode/skills` → симлинк на agents |
|
||||||
| **Codex** | `~/.codex/skills/` → симлинки на agents |
|
| **Codex** | `~/.codex/skills/` → симлинки на agents |
|
||||||
| **Gemini** | Политики для CLI: pre-approved readonly команды |
|
| **Gemini** | Политики для CLI: pre-approved readonly команды |
|
||||||
@@ -198,7 +198,7 @@ echo '127.0.0.1 hproxy.local' | sudo tee -a /etc/hosts
|
|||||||
| `~/.gitconfig.local` | Персональные Git-настройки (`user.name`, `core.editor` и т.д.) |
|
| `~/.gitconfig.local` | Персональные Git-настройки (`user.name`, `core.editor` и т.д.) |
|
||||||
| `~/.config/powershell/profile.extra.ps1` | Локальные дополнения к pwsh-профилю |
|
| `~/.config/powershell/profile.extra.ps1` | Локальные дополнения к pwsh-профилю |
|
||||||
| `~/.config/powershell/profile.d/*.ps1` | Фрагменты, загружаемые по алфавиту |
|
| `~/.config/powershell/profile.d/*.ps1` | Фрагменты, загружаемые по алфавиту |
|
||||||
| `~/.claude/settings.local.json` | Машинно-специфичные настройки Claude Code (`model`, `effortLevel`) |
|
| `~/.claude/settings.json` | Управляется Claude Code: `model`, `effortLevel`, «always allow» permissions |
|
||||||
|
|
||||||
## 💡 Дизайн-решения
|
## 💡 Дизайн-решения
|
||||||
|
|
||||||
@@ -208,5 +208,5 @@ stow проще и покрывает весь Linux/WSL workflow. chezmoi ну
|
|||||||
**Почему Windows PowerShell 5.1 не трогается?**
|
**Почему Windows PowerShell 5.1 не трогается?**
|
||||||
Форсирование UTF-8 и изменение профиля PS 5.1 может сломать старые скрипты, `.bat`/`.cmd` и консольные утилиты. PS 7 — основной shell, PS 5.1 — режим совместимости.
|
Форсирование UTF-8 и изменение профиля PS 5.1 может сломать старые скрипты, `.bat`/`.cmd` и консольные утилиты. PS 7 — основной shell, PS 5.1 — режим совместимости.
|
||||||
|
|
||||||
**Почему Claude `settings.json` копируется, а не симлинкается?**
|
**Почему Claude `settings.local.json` — симлинк, а `settings.json` — нет?**
|
||||||
Claude Code перезаписывает `settings.json` при любом изменении настроек. Если файл — симлинк на tracked файл в git, это создаёт нежелательные diff-ы. Копия с `chmod 444` защищает от перезаписи; машинно-специфичные настройки уходят в `settings.local.json`.
|
Claude Code перезаписывает `settings.json` при каждом «always allow» — этот файл принадлежит Claude Code. Базовые permissions и statusLine живут в `settings.local.json`, который Claude Code только читает и мержит с `settings.json`. Симлинк из `~/.claude/settings.local.json` на файл в репозитории безопасен. Машинно-специфичные настройки (`model`, `effortLevel`) — в `settings.json`, управляются через Claude Code.
|
||||||
@@ -28,7 +28,7 @@ echo "════════════════════════
|
|||||||
for pkg_dir in "$DOTFILES_DIR"/*/; do
|
for pkg_dir in "$DOTFILES_DIR"/*/; do
|
||||||
pkg=$(basename "$pkg_dir")
|
pkg=$(basename "$pkg_dir")
|
||||||
[[ "$pkg" == .* ]] && continue
|
[[ "$pkg" == .* ]] && continue
|
||||||
# claude управляется вручную (копирование settings.json), не через stow
|
# claude управляется вручную (симлинк settings.local.json), не через stow
|
||||||
[[ "$pkg" == "claude" ]] && continue
|
[[ "$pkg" == "claude" ]] && continue
|
||||||
|
|
||||||
# Точки входа — элементы первого уровня внутри пакета
|
# Точки входа — элементы первого уровня внутри пакета
|
||||||
@@ -110,30 +110,21 @@ if [ -f "$install_script" ]; then
|
|||||||
err "~/.claude — не найден"
|
err "~/.claude — не найден"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Claude Code: settings.json должен быть копией, не симлинком
|
# Claude Code: settings.local.json должен быть симлинком на репозиторий
|
||||||
if [ -L "$HOME/.claude/settings.json" ]; then
|
if [ -L "$HOME/.claude/settings.local.json" ]; then
|
||||||
err "~/.claude/settings.json — симлинк (должна быть копия)"
|
local_target=$(readlink -f "$HOME/.claude/settings.local.json" 2>/dev/null || true)
|
||||||
elif [ -f "$HOME/.claude/settings.json" ]; then
|
local_expected=$(readlink -f "$DOTFILES_DIR/claude/.claude/settings.local.json" 2>/dev/null || true)
|
||||||
pass "~/.claude/settings.json — файл (копия)"
|
if [ "$local_target" = "$local_expected" ]; then
|
||||||
else
|
pass "~/.claude/settings.local.json → репозиторий"
|
||||||
err "~/.claude/settings.json — не найден"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# chmod 444 на settings.json в репозитории
|
|
||||||
if [ -f "$DOTFILES_DIR/claude/.claude/settings.json" ]; then
|
|
||||||
perms=$(stat -c %a "$DOTFILES_DIR/claude/.claude/settings.json" 2>/dev/null || true)
|
|
||||||
if [ "$perms" = "444" ]; then
|
|
||||||
pass "claude/settings.json — chmod 444"
|
|
||||||
else
|
else
|
||||||
skip "claude/settings.json — chmod $perms (ожидался 444)"
|
err "~/.claude/settings.local.json → $local_target (ожидался $local_expected)"
|
||||||
fi
|
fi
|
||||||
fi
|
elif [ -f "$HOME/.claude/settings.local.json" ]; then
|
||||||
|
err "~/.claude/settings.local.json — обычный файл (должен быть симлинк). Запустите install.sh"
|
||||||
# settings.local.json
|
elif [ -e "$HOME/.claude/settings.local.json" ]; then
|
||||||
if [ -f "$HOME/.claude/settings.local.json" ]; then
|
err "~/.claude/settings.local.json — не файл и не симлинк. Удалите вручную и запустите install.sh"
|
||||||
pass "~/.claude/settings.local.json существует"
|
|
||||||
else
|
else
|
||||||
skip "~/.claude/settings.local.json — не найден (создайте через install.sh)"
|
err "~/.claude/settings.local.json — не найден (запустите install.sh)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# skills симлинк
|
# skills симлинк
|
||||||
|
|||||||
@@ -2,4 +2,4 @@
|
|||||||
*
|
*
|
||||||
!skills
|
!skills
|
||||||
!.gitignore
|
!.gitignore
|
||||||
!settings.json
|
!settings.local.json
|
||||||
@@ -69,6 +69,12 @@
|
|||||||
"Bash(docker version*)",
|
"Bash(docker version*)",
|
||||||
"Bash(docker compose ps*)",
|
"Bash(docker compose ps*)",
|
||||||
"Bash(docker compose logs*)",
|
"Bash(docker compose logs*)",
|
||||||
|
"Bash(docker compose:*)",
|
||||||
|
"Bash(docker restart:*)",
|
||||||
|
"Bash(docker exec:*)",
|
||||||
|
"Bash(docker run:*)",
|
||||||
|
"Bash(docker stop:*)",
|
||||||
|
"Bash(docker rm:*)",
|
||||||
"Bash(curl *)",
|
"Bash(curl *)",
|
||||||
"Bash(wget *)",
|
"Bash(wget *)",
|
||||||
"Bash(mkdir *)",
|
"Bash(mkdir *)",
|
||||||
@@ -92,7 +98,15 @@
|
|||||||
"Bash(pytest *)",
|
"Bash(pytest *)",
|
||||||
"Bash(node *)",
|
"Bash(node *)",
|
||||||
"Bash(deno *)",
|
"Bash(deno *)",
|
||||||
"Bash(stow *)"
|
"Bash(stow *)",
|
||||||
|
"Bash(gh api:*)",
|
||||||
|
"Bash(gh search:*)",
|
||||||
|
"Bash(gh issue:*)",
|
||||||
|
"Bash(gh repo:*)",
|
||||||
|
"Bash(codex *)",
|
||||||
|
"WebSearch",
|
||||||
|
"mcp__claude_ai_Context7__resolve-library-id",
|
||||||
|
"Write(/tmp/claude-plan-*)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"statusLine": {
|
"statusLine": {
|
||||||
+21
-30
@@ -160,7 +160,7 @@ migrate_claude_stow_symlink() {
|
|||||||
local base
|
local base
|
||||||
base=$(basename "$item")
|
base=$(basename "$item")
|
||||||
# git-tracked файлы остаются в репозитории
|
# git-tracked файлы остаются в репозитории
|
||||||
case "$base" in .gitignore|settings.json|skills) continue ;; esac
|
case "$base" in .gitignore|settings.local.json|skills) continue ;; esac
|
||||||
mv "$item" "$HOME/.claude/" 2>/dev/null || true
|
mv "$item" "$HOME/.claude/" 2>/dev/null || true
|
||||||
done
|
done
|
||||||
info "Рантайм-данные перенесены в ~/.claude/"
|
info "Рантайм-данные перенесены в ~/.claude/"
|
||||||
@@ -271,8 +271,10 @@ if [ -f "$OPENCODE_JSON_SRC" ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Claude Code ──────────────────────────────────────────────────────
|
# ── Claude Code ──────────────────────────────────────────────────────
|
||||||
# settings.json КОПИРУЕТСЯ (не линкуется), чтобы Claude Code не модифицировал
|
# settings.local.json ЛИНКУЕТСЯ из репозитория (базовые permissions + statusLine).
|
||||||
# файл в репозитории. Локальные настройки — в settings.local.json.
|
# Claude Code не модифицирует settings.local.json — только читает и мержит
|
||||||
|
# с settings.json, который Claude Code создаёт и обновляет самостоятельно.
|
||||||
|
# Машинно-специфичные настройки (model, effortLevel) — в settings.json.
|
||||||
|
|
||||||
migrate_claude_stow_symlink
|
migrate_claude_stow_symlink
|
||||||
mkdir -p "$HOME/.claude"
|
mkdir -p "$HOME/.claude"
|
||||||
@@ -282,35 +284,24 @@ if [ -d "$SKILLS_SRC" ]; then
|
|||||||
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
|
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# settings.json — копия из репозитория (не симлинк!)
|
# settings.local.json — базовые настройки из репозитория (симлинк)
|
||||||
CLAUDE_SETTINGS_SRC="$DOTFILES_DIR/claude/.claude/settings.json"
|
CLAUDE_LOCAL_SRC="$DOTFILES_DIR/claude/.claude/settings.local.json"
|
||||||
CLAUDE_SETTINGS_DST="$HOME/.claude/settings.json"
|
CLAUDE_LOCAL_DST="$HOME/.claude/settings.local.json"
|
||||||
if [ -f "$CLAUDE_SETTINGS_SRC" ]; then
|
if [ -f "$CLAUDE_LOCAL_SRC" ]; then
|
||||||
chmod 444 "$CLAUDE_SETTINGS_SRC"
|
if [ -L "$CLAUDE_LOCAL_DST" ]; then
|
||||||
if "$GIT_BIN" -C "$DOTFILES_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
ln -sfnT "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
|
||||||
if ! "$GIT_BIN" -C "$DOTFILES_DIR" show "HEAD:claude/.claude/settings.json" > "$CLAUDE_SETTINGS_DST"; then
|
info "settings.local.json → репозиторий"
|
||||||
warn "Не удалось прочитать committed settings.json, копирую текущий файл из репозитория"
|
elif [ -f "$CLAUDE_LOCAL_DST" ]; then
|
||||||
cp -f "$CLAUDE_SETTINGS_SRC" "$CLAUDE_SETTINGS_DST"
|
backup_existing_path "$CLAUDE_LOCAL_DST" "settings.local.json"
|
||||||
fi
|
ln -s "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
|
||||||
|
info "settings.local.json → репозиторий"
|
||||||
|
info "Машинно-специфичные настройки (model, effortLevel) теперь задаются через Claude Code"
|
||||||
|
elif [ -d "$CLAUDE_LOCAL_DST" ] || [ -e "$CLAUDE_LOCAL_DST" ]; then
|
||||||
|
warn "$CLAUDE_LOCAL_DST уже существует (не файл и не симлинк), удалите вручную"
|
||||||
else
|
else
|
||||||
cp -f "$CLAUDE_SETTINGS_SRC" "$CLAUDE_SETTINGS_DST"
|
ln -s "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
|
||||||
|
info "settings.local.json → репозиторий"
|
||||||
fi
|
fi
|
||||||
chmod 644 "$CLAUDE_SETTINGS_DST"
|
|
||||||
info "settings.json скопирован (оригинал защищён chmod 444)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# settings.local.json — локальные переопределения (не в git)
|
|
||||||
CLAUDE_LOCAL="$HOME/.claude/settings.local.json"
|
|
||||||
if [ ! -f "$CLAUDE_LOCAL" ]; then
|
|
||||||
info "Создаю $CLAUDE_LOCAL (локальные настройки Claude)..."
|
|
||||||
cat > "$CLAUDE_LOCAL" <<'EOF'
|
|
||||||
{
|
|
||||||
"model": "opus",
|
|
||||||
"effortLevel": "high"
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
else
|
|
||||||
warn "$CLAUDE_LOCAL уже существует, пропускаю"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Bash: .bashrc ────────────────────────────────────────────────────
|
# ── Bash: .bashrc ────────────────────────────────────────────────────
|
||||||
|
|||||||
+62
-10
@@ -101,23 +101,72 @@ test_fresh_install_uses_home_target() {
|
|||||||
assert_not_exists "$fixture/.bash_custom"
|
assert_not_exists "$fixture/.bash_custom"
|
||||||
}
|
}
|
||||||
|
|
||||||
test_dirty_repo_settings_copy_uses_committed_version() {
|
test_settings_local_is_symlinked_to_repo() {
|
||||||
local fixture repo_root home_dir
|
local fixture repo_root home_dir
|
||||||
fixture="$(setup_fixture)"
|
fixture="$(setup_fixture)"
|
||||||
repo_root="$fixture/repo"
|
repo_root="$fixture/repo"
|
||||||
home_dir="$fixture/home"
|
home_dir="$fixture/home"
|
||||||
mkdir -p "$home_dir"
|
mkdir -p "$home_dir"
|
||||||
|
|
||||||
chmod u+w "$repo_root/claude/.claude/settings.json"
|
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
|
||||||
cat > "$repo_root/claude/.claude/settings.json" <<'EOF'
|
|
||||||
{"source":"working-tree"}
|
|
||||||
EOF
|
|
||||||
|
|
||||||
FAKE_GIT_SHOW_CONTENT='{"source":"head"}' \
|
[ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to be a symlink"
|
||||||
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
|
local target
|
||||||
|
target="$(readlink -f "$home_dir/.claude/settings.local.json")"
|
||||||
|
local expected
|
||||||
|
expected="$(readlink -f "$repo_root/claude/.claude/settings.local.json")"
|
||||||
|
[ "$target" = "$expected" ] || fail "settings.local.json points to $target, expected $expected"
|
||||||
|
}
|
||||||
|
|
||||||
assert_file_contains "$home_dir/.claude/settings.json" '"source":"head"'
|
test_settings_local_backup_on_existing_file() {
|
||||||
assert_file_contains "$repo_root/claude/.claude/settings.json" '"source":"working-tree"'
|
local fixture repo_root home_dir
|
||||||
|
fixture="$(setup_fixture)"
|
||||||
|
repo_root="$fixture/repo"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
mkdir -p "$home_dir/.claude"
|
||||||
|
echo '{"model":"sonnet"}' > "$home_dir/.claude/settings.local.json"
|
||||||
|
|
||||||
|
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
|
||||||
|
|
||||||
|
[ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json to become a symlink"
|
||||||
|
local backup
|
||||||
|
backup="$(find "$home_dir/.claude" -maxdepth 1 -name 'settings.local.json.bak.*' -print -quit)"
|
||||||
|
[ -n "$backup" ] || fail "Expected backup for existing settings.local.json"
|
||||||
|
assert_file_contains "$backup" '"model":"sonnet"'
|
||||||
|
}
|
||||||
|
|
||||||
|
test_settings_local_dir_is_skipped() {
|
||||||
|
local fixture repo_root home_dir
|
||||||
|
fixture="$(setup_fixture)"
|
||||||
|
repo_root="$fixture/repo"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
mkdir -p "$home_dir/.claude/settings.local.json"
|
||||||
|
|
||||||
|
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
|
||||||
|
|
||||||
|
[ -d "$home_dir/.claude/settings.local.json" ] || fail "Expected directory to be preserved (not replaced)"
|
||||||
|
}
|
||||||
|
|
||||||
|
test_stow_migration_preserves_legacy_settings_local() {
|
||||||
|
local fixture repo_root home_dir stow_target
|
||||||
|
fixture="$(setup_fixture)"
|
||||||
|
repo_root="$fixture/repo"
|
||||||
|
home_dir="$fixture/home"
|
||||||
|
mkdir -p "$home_dir"
|
||||||
|
|
||||||
|
# Simulate old stow layout: ~/.claude → repo dir
|
||||||
|
stow_target="$repo_root/claude/.claude"
|
||||||
|
ln -s "$stow_target" "$home_dir/.claude"
|
||||||
|
# Put a legacy settings.local.json in the stow target (would have been gitignored before)
|
||||||
|
echo '{"model":"opus"}' > "$stow_target/legacy-local.json"
|
||||||
|
|
||||||
|
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
|
||||||
|
|
||||||
|
[ -d "$home_dir/.claude" ] || fail "Expected ~/.claude to become a directory"
|
||||||
|
[ ! -L "$home_dir/.claude" ] || fail "Expected ~/.claude to stop being a symlink"
|
||||||
|
[ -L "$home_dir/.claude/settings.local.json" ] || fail "Expected settings.local.json symlink after migration"
|
||||||
|
# legacy-local.json should have been moved from stow dir to real ~/.claude
|
||||||
|
[ -f "$home_dir/.claude/legacy-local.json" ] || fail "Expected legacy file to be migrated"
|
||||||
}
|
}
|
||||||
|
|
||||||
test_rerun_does_not_duplicate_bashrc_line() {
|
test_rerun_does_not_duplicate_bashrc_line() {
|
||||||
@@ -332,7 +381,10 @@ main() {
|
|||||||
command -v stow >/dev/null 2>&1 || fail "stow is required for tests"
|
command -v stow >/dev/null 2>&1 || fail "stow is required for tests"
|
||||||
|
|
||||||
test_fresh_install_uses_home_target
|
test_fresh_install_uses_home_target
|
||||||
test_dirty_repo_settings_copy_uses_committed_version
|
test_settings_local_is_symlinked_to_repo
|
||||||
|
test_settings_local_backup_on_existing_file
|
||||||
|
test_settings_local_dir_is_skipped
|
||||||
|
test_stow_migration_preserves_legacy_settings_local
|
||||||
test_rerun_does_not_duplicate_bashrc_line
|
test_rerun_does_not_duplicate_bashrc_line
|
||||||
test_repairs_nested_skills_directory_bug
|
test_repairs_nested_skills_directory_bug
|
||||||
test_preserves_nonempty_skills_directory
|
test_preserves_nonempty_skills_directory
|
||||||
|
|||||||
Reference in new issue
Block a user