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 редактора |
|
||||
| `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)
|
||||
|
||||
@@ -70,7 +70,7 @@ Chezmoi использует отдельный source dir `chezmoi/` внутр
|
||||
| Инструмент | Что настраивается |
|
||||
|------------|-------------------|
|
||||
| **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 |
|
||||
| **Codex** | `~/.codex/skills/` → симлинки на agents |
|
||||
| **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` и т.д.) |
|
||||
| `~/.config/powershell/profile.extra.ps1` | Локальные дополнения к pwsh-профилю |
|
||||
| `~/.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 не трогается?**
|
||||
Форсирование UTF-8 и изменение профиля PS 5.1 может сломать старые скрипты, `.bat`/`.cmd` и консольные утилиты. PS 7 — основной shell, PS 5.1 — режим совместимости.
|
||||
|
||||
**Почему Claude `settings.json` копируется, а не симлинкается?**
|
||||
Claude Code перезаписывает `settings.json` при любом изменении настроек. Если файл — симлинк на tracked файл в git, это создаёт нежелательные diff-ы. Копия с `chmod 444` защищает от перезаписи; машинно-специфичные настройки уходят в `settings.local.json`.
|
||||
**Почему Claude `settings.local.json` — симлинк, а `settings.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
|
||||
pkg=$(basename "$pkg_dir")
|
||||
[[ "$pkg" == .* ]] && continue
|
||||
# claude управляется вручную (копирование settings.json), не через stow
|
||||
# claude управляется вручную (симлинк settings.local.json), не через stow
|
||||
[[ "$pkg" == "claude" ]] && continue
|
||||
|
||||
# Точки входа — элементы первого уровня внутри пакета
|
||||
@@ -110,30 +110,21 @@ if [ -f "$install_script" ]; then
|
||||
err "~/.claude — не найден"
|
||||
fi
|
||||
|
||||
# Claude Code: settings.json должен быть копией, не симлинком
|
||||
if [ -L "$HOME/.claude/settings.json" ]; then
|
||||
err "~/.claude/settings.json — симлинк (должна быть копия)"
|
||||
elif [ -f "$HOME/.claude/settings.json" ]; then
|
||||
pass "~/.claude/settings.json — файл (копия)"
|
||||
else
|
||||
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"
|
||||
# Claude Code: settings.local.json должен быть симлинком на репозиторий
|
||||
if [ -L "$HOME/.claude/settings.local.json" ]; then
|
||||
local_target=$(readlink -f "$HOME/.claude/settings.local.json" 2>/dev/null || true)
|
||||
local_expected=$(readlink -f "$DOTFILES_DIR/claude/.claude/settings.local.json" 2>/dev/null || true)
|
||||
if [ "$local_target" = "$local_expected" ]; then
|
||||
pass "~/.claude/settings.local.json → репозиторий"
|
||||
else
|
||||
skip "claude/settings.json — chmod $perms (ожидался 444)"
|
||||
err "~/.claude/settings.local.json → $local_target (ожидался $local_expected)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# settings.local.json
|
||||
if [ -f "$HOME/.claude/settings.local.json" ]; then
|
||||
pass "~/.claude/settings.local.json существует"
|
||||
elif [ -f "$HOME/.claude/settings.local.json" ]; then
|
||||
err "~/.claude/settings.local.json — обычный файл (должен быть симлинк). Запустите install.sh"
|
||||
elif [ -e "$HOME/.claude/settings.local.json" ]; then
|
||||
err "~/.claude/settings.local.json — не файл и не симлинк. Удалите вручную и запустите install.sh"
|
||||
else
|
||||
skip "~/.claude/settings.local.json — не найден (создайте через install.sh)"
|
||||
err "~/.claude/settings.local.json — не найден (запустите install.sh)"
|
||||
fi
|
||||
|
||||
# skills симлинк
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
*
|
||||
!skills
|
||||
!.gitignore
|
||||
!settings.json
|
||||
!settings.local.json
|
||||
@@ -69,6 +69,12 @@
|
||||
"Bash(docker version*)",
|
||||
"Bash(docker compose ps*)",
|
||||
"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(wget *)",
|
||||
"Bash(mkdir *)",
|
||||
@@ -92,7 +98,15 @@
|
||||
"Bash(pytest *)",
|
||||
"Bash(node *)",
|
||||
"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": {
|
||||
+21
-30
@@ -160,7 +160,7 @@ migrate_claude_stow_symlink() {
|
||||
local base
|
||||
base=$(basename "$item")
|
||||
# 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
|
||||
done
|
||||
info "Рантайм-данные перенесены в ~/.claude/"
|
||||
@@ -271,8 +271,10 @@ if [ -f "$OPENCODE_JSON_SRC" ]; then
|
||||
fi
|
||||
|
||||
# ── Claude Code ──────────────────────────────────────────────────────
|
||||
# settings.json КОПИРУЕТСЯ (не линкуется), чтобы Claude Code не модифицировал
|
||||
# файл в репозитории. Локальные настройки — в settings.local.json.
|
||||
# settings.local.json ЛИНКУЕТСЯ из репозитория (базовые permissions + statusLine).
|
||||
# Claude Code не модифицирует settings.local.json — только читает и мержит
|
||||
# с settings.json, который Claude Code создаёт и обновляет самостоятельно.
|
||||
# Машинно-специфичные настройки (model, effortLevel) — в settings.json.
|
||||
|
||||
migrate_claude_stow_symlink
|
||||
mkdir -p "$HOME/.claude"
|
||||
@@ -282,35 +284,24 @@ if [ -d "$SKILLS_SRC" ]; then
|
||||
ensure_symlink "$SKILLS_SRC" "$HOME/.claude/skills"
|
||||
fi
|
||||
|
||||
# settings.json — копия из репозитория (не симлинк!)
|
||||
CLAUDE_SETTINGS_SRC="$DOTFILES_DIR/claude/.claude/settings.json"
|
||||
CLAUDE_SETTINGS_DST="$HOME/.claude/settings.json"
|
||||
if [ -f "$CLAUDE_SETTINGS_SRC" ]; then
|
||||
chmod 444 "$CLAUDE_SETTINGS_SRC"
|
||||
if "$GIT_BIN" -C "$DOTFILES_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
if ! "$GIT_BIN" -C "$DOTFILES_DIR" show "HEAD:claude/.claude/settings.json" > "$CLAUDE_SETTINGS_DST"; then
|
||||
warn "Не удалось прочитать committed settings.json, копирую текущий файл из репозитория"
|
||||
cp -f "$CLAUDE_SETTINGS_SRC" "$CLAUDE_SETTINGS_DST"
|
||||
fi
|
||||
# settings.local.json — базовые настройки из репозитория (симлинк)
|
||||
CLAUDE_LOCAL_SRC="$DOTFILES_DIR/claude/.claude/settings.local.json"
|
||||
CLAUDE_LOCAL_DST="$HOME/.claude/settings.local.json"
|
||||
if [ -f "$CLAUDE_LOCAL_SRC" ]; then
|
||||
if [ -L "$CLAUDE_LOCAL_DST" ]; then
|
||||
ln -sfnT "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
|
||||
info "settings.local.json → репозиторий"
|
||||
elif [ -f "$CLAUDE_LOCAL_DST" ]; then
|
||||
backup_existing_path "$CLAUDE_LOCAL_DST" "settings.local.json"
|
||||
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
|
||||
cp -f "$CLAUDE_SETTINGS_SRC" "$CLAUDE_SETTINGS_DST"
|
||||
ln -s "$CLAUDE_LOCAL_SRC" "$CLAUDE_LOCAL_DST"
|
||||
info "settings.local.json → репозиторий"
|
||||
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
|
||||
|
||||
# ── Bash: .bashrc ────────────────────────────────────────────────────
|
||||
|
||||
+62
-10
@@ -101,23 +101,72 @@ test_fresh_install_uses_home_target() {
|
||||
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
|
||||
fixture="$(setup_fixture)"
|
||||
repo_root="$fixture/repo"
|
||||
home_dir="$fixture/home"
|
||||
mkdir -p "$home_dir"
|
||||
|
||||
chmod u+w "$repo_root/claude/.claude/settings.json"
|
||||
cat > "$repo_root/claude/.claude/settings.json" <<'EOF'
|
||||
{"source":"working-tree"}
|
||||
EOF
|
||||
run_install "$repo_root" "$home_dir" env WSL_DISTRO_NAME= INSTALL_INTERACTIVE_MODE=never >/tmp/test-install.out
|
||||
|
||||
FAKE_GIT_SHOW_CONTENT='{"source":"head"}' \
|
||||
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 be a symlink"
|
||||
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"'
|
||||
assert_file_contains "$repo_root/claude/.claude/settings.json" '"source":"working-tree"'
|
||||
test_settings_local_backup_on_existing_file() {
|
||||
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() {
|
||||
@@ -332,7 +381,10 @@ main() {
|
||||
command -v stow >/dev/null 2>&1 || fail "stow is required for tests"
|
||||
|
||||
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_repairs_nested_skills_directory_bug
|
||||
test_preserves_nonempty_skills_directory
|
||||
|
||||
Reference in new issue
Block a user