From b0e1889be1862b0e26831861a96d65ac0613a407 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:20:57 -0400 Subject: [PATCH 1/5] chore: add shared Claude and Codex Git instructions --- README.md | 3 +++ agent-instructions.md | 13 +++++++++++++ assimilate.sh | 2 ++ 3 files changed, 18 insertions(+) create mode 100644 agent-instructions.md diff --git a/README.md b/README.md index 49af985..87e465c 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,9 @@ cd into the `dotfiles` directory, and then run: ./assimilate.sh ``` +The installer shares `agent-instructions.md` with Claude Code and Codex by +linking it to `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`. + ### Tmux Plugin Manager - Install Tmux Plugin Manager ([Github](https://github.com/tmux-plugins/tpm#tmux-plugin-manager)) - Install tmux packages with `prefix + I` diff --git a/agent-instructions.md b/agent-instructions.md new file mode 100644 index 0000000..0006e64 --- /dev/null +++ b/agent-instructions.md @@ -0,0 +1,13 @@ +# Commit Instructions + +Commit messages should be concise, just one sentence + +# Pull Request Instructions + +Create pull requests with the simple format + +## Summary + + +## Test Plan + diff --git a/assimilate.sh b/assimilate.sh index e445ec1..43a69fb 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -44,6 +44,8 @@ sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/confi sym tmux-powerline/config.sh .config/tmux-powerline/config.sh sym tmux-powerline/themes/theme.sh .config/tmux-powerline/themes/theme.sh sym claude/statusline.sh .claude/statusline.sh +sym agent-instructions.md .claude/CLAUDE.md +sym agent-instructions.md .codex/AGENTS.md sym zed/keymap.json .config/zed/keymap.json brew bundle install From a94c4a15a690e6da110b627a9f5cbf001388f6f3 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:02:17 -0400 Subject: [PATCH 2/5] docs: add code formatting instructions --- agent-instructions.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/agent-instructions.md b/agent-instructions.md index 0006e64..08a891e 100644 --- a/agent-instructions.md +++ b/agent-instructions.md @@ -11,3 +11,9 @@ Create pull requests with the simple format ## Test Plan + +# Code Formatting Instructions + +Single-line commands or code within a sentence should be wrapped in backticks + +Multi-line commands or code should use fenced code blocks with triple backticks From 7a62c87d41981938d97673893479fce7a9d08533 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 20 Aug 2026 09:06:05 -0400 Subject: [PATCH 3/5] chore: version concise Claude output settings --- README.md | 6 +- assimilate.sh | 1 + claude/settings.json | 156 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 claude/settings.json diff --git a/README.md b/README.md index 87e465c..f69e2f9 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,10 @@ cd into the `dotfiles` directory, and then run: ./assimilate.sh ``` -The installer shares `agent-instructions.md` with Claude Code and Codex by -linking it to `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`. +The installer links the version-controlled Claude Code settings to +`~/.claude/settings.json`. It also shares `agent-instructions.md` with Claude +Code and Codex by linking it to `~/.claude/CLAUDE.md` and +`~/.codex/AGENTS.md`. ### Tmux Plugin Manager - Install Tmux Plugin Manager ([Github](https://github.com/tmux-plugins/tpm#tmux-plugin-manager)) diff --git a/assimilate.sh b/assimilate.sh index 43a69fb..1da7f86 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -44,6 +44,7 @@ sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/confi sym tmux-powerline/config.sh .config/tmux-powerline/config.sh sym tmux-powerline/themes/theme.sh .config/tmux-powerline/themes/theme.sh sym claude/statusline.sh .claude/statusline.sh +sym claude/settings.json .claude/settings.json sym agent-instructions.md .claude/CLAUDE.md sym agent-instructions.md .codex/AGENTS.md sym zed/keymap.json .config/zed/keymap.json diff --git a/claude/settings.json b/claude/settings.json new file mode 100644 index 0000000..e0f7602 --- /dev/null +++ b/claude/settings.json @@ -0,0 +1,156 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "cleanupPeriodDays": 365, + "attribution": { + "commit": "", + "pr": "", + "sessionUrl": false + }, + "permissions": { + "allow": [ + "Bash(git fetch *)", + "Bash(ls *)", + "Bash(xargs *)", + "Bash(git mv *)", + "Bash(for chain in axelar boba corn monad nova memecore ronin multiversx linea algorand helium hivemapper hedera near jupiter walrus momentum flare metis iotex manta ink soneium astar janus_henderson moonbeam berachain unichain worldchain sonic abstract bob b3 mode apechain plume story codex hyperevm ripple movement katana conflux decentraland core plasma tempo_andantino provenance tezos)", + "Bash(if [ -d \"/Users/alexkan/work/gokustats-back-end/artemis_dagster/extract_load/primary_sources/$chain\" ])", + "Bash(then)", + "Bash(echo *)", + "Bash(for chain in axelar memecore ronin multiversx linea algorand helium hivemapper hedera near jupiter walrus momentum flare metis iotex manta ink soneium astar moonbeam berachain unichain worldchain sonic abstract bob b3 mode apechain plume story codex hyperevm ripple conflux core provenance)", + "Bash(gh pr list *)", + "Bash(git *)", + "Bash(gh *)", + "Bash(cat *)", + "Bash(head *)", + "Bash(tail *)", + "Bash(wc *)", + "Bash(which *)", + "Bash(pwd *)", + "Bash(find *)", + "Bash(grep *)", + "Bash(mkdir *)", + "Bash(touch *)", + "Bash(diff *)", + "Bash(chmod *)", + "Bash(cp *)", + "Bash(mv *)", + "Bash(curl *)", + "Bash(lsof *)", + "Bash(test *)", + "Read", + "Write", + "Edit", + "Grep", + "Glob", + "WebFetch", + "WebSearch", + "Skill(session-handoff)", + "Skill(session-handoff:*)", + "Skill(context-resume)", + "Skill(context-resume:*)", + "Skill(tech-debt)", + "Skill(tech-debt:*)", + "Skill(security-audit)", + "Skill(security-audit:*)", + "Skill(product-enhance)", + "Skill(product-enhance:*)", + "Skill(docs-review)", + "Skill(docs-review:*)", + "Skill(frontend-performance-audit)", + "Skill(frontend-performance-audit:*)", + "Edit(/tmp/**)", + "Edit(~/.claude/**)" + ], + "defaultMode": "auto" + }, + "model": "opus[1m]", + "outputStyle": "Concise", + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "afplay /System/Library/Sounds/Glass.aiff" + }, + { + "type": "command", + "command": "INFO=$(tmux display-message -p 'Window #I, Pane #P' 2>/dev/null || echo '?'); osascript -e \"display notification \\\"Claude is ready! ($INFO)\\\" with title \\\"Claude Code\\\"\"" + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "~/.claude/hooks/pretooluse-permissions.sh" + } + ] + } + ] + }, + "statusLine": { + "type": "command", + "command": "~/.claude/statusline.sh", + "padding": 0 + }, + "enabledPlugins": { + "pyright-lsp@claude-plugins-official": true, + "github@claude-plugins-official": true, + "code-review@claude-plugins-official": true, + "frontend-design@claude-plugins-official": true, + "security-guidance@claude-plugins-official": true, + "playwright@claude-plugins-official": true, + "typescript-lsp@claude-plugins-official": true, + "rust-analyzer-lsp@claude-plugins-official": true, + "lua-lsp@claude-plugins-official": true, + "pr-review-toolkit@claude-plugins-official": true, + "ralph-wiggum@claude-plugins-official": true, + "superpowers@claude-plugins-official": true, + "skill-creator@claude-plugins-official": true, + "telegram@claude-plugins-official": true, + "posthog@claude-plugins-official": true, + "planetscale@planetscale": true, + "artemis-skills@artemis-skills": true + }, + "extraKnownMarketplaces": { + "claude-plugins-official": { + "source": { + "source": "github", + "repo": "anthropics/claude-plugins-official" + } + }, + "planetscale": { + "source": { + "source": "github", + "repo": "planetscale/claude-plugin" + } + }, + "artemis-skills": { + "source": { + "source": "github", + "repo": "Artemis-xyz/artemis-skills" + } + } + }, + "sandbox": { + "enabled": true, + "autoAllowBashIfSandboxed": true, + "allowUnsandboxedCommands": true, + "excludedCommands": [] + }, + "alwaysThinkingEnabled": true, + "effortLevel": "high", + "skipDangerousModePermissionPrompt": true, + "skipWorkflowUsageWarning": true, + "editorMode": "vim", + "verbose": false, + "preferredNotifChannel": "ghostty", + "teammateMode": "auto", + "agentPushNotifEnabled": true, + "skipAutoPermissionPrompt": true, + "voiceEnabled": true +} From fcd5496ebb6e3cc9030e41726b476150afea0ca1 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Thu, 20 Aug 2026 18:23:03 -0400 Subject: [PATCH 4/5] fix: resolve dotfiles from the installer location --- assimilate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assimilate.sh b/assimilate.sh index 1da7f86..925af45 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -3,7 +3,7 @@ set -ex PREFIX="$HOME" -DOTFILES="$PREFIX/dotfiles" +DOTFILES=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) BACKUPS="$PREFIX/backups" function sym () { From 9c3dc31817db657c03335cfae3f70f32c2024fe4 Mon Sep 17 00:00:00 2001 From: Alex Kan <29241719+akan72@users.noreply.github.com> Date: Sat, 22 Aug 2026 12:11:13 -0400 Subject: [PATCH 5/5] refactor: manage portable agent settings safely --- Brewfile | 1 + README.md | 41 ++++- assimilate.sh | 4 +- claude/settings.json | 156 -------------------- claude/settings.managed.json | 14 ++ claude/sync-settings.sh | 41 +++++ codex/config.managed.toml | 23 +++ codex/sync-config.sh | 59 ++++++++ tests/assimilate-path.sh | 47 ++++++ tests/claude-sync-settings.sh | 57 +++++++ tests/codex-sync-config.sh | 70 +++++++++ tests/fixtures/bin/brew | 3 + tests/fixtures/bin/codex | 4 + tests/fixtures/claude/invalid-settings.json | 3 + tests/fixtures/claude/settings.json | 10 ++ tests/fixtures/codex/config.toml | 12 ++ tests/fixtures/codex/invalid-config.toml | 1 + 17 files changed, 385 insertions(+), 161 deletions(-) delete mode 100644 claude/settings.json create mode 100644 claude/settings.managed.json create mode 100755 claude/sync-settings.sh create mode 100644 codex/config.managed.toml create mode 100755 codex/sync-config.sh create mode 100755 tests/assimilate-path.sh create mode 100755 tests/claude-sync-settings.sh create mode 100755 tests/codex-sync-config.sh create mode 100755 tests/fixtures/bin/brew create mode 100755 tests/fixtures/bin/codex create mode 100644 tests/fixtures/claude/invalid-settings.json create mode 100644 tests/fixtures/claude/settings.json create mode 100644 tests/fixtures/codex/config.toml create mode 100644 tests/fixtures/codex/invalid-config.toml diff --git a/Brewfile b/Brewfile index 97fa1ef..d8f6b8e 100644 --- a/Brewfile +++ b/Brewfile @@ -18,6 +18,7 @@ brew "pyenv" brew "tmux" brew "tree" brew "wget" +brew "yq" brew "zsh-autosuggestions" brew "zsh-syntax-highlighting" cask "font-hack-nerd-font" diff --git a/README.md b/README.md index f69e2f9..022b899 100644 --- a/README.md +++ b/README.md @@ -45,10 +45,43 @@ cd into the `dotfiles` directory, and then run: ./assimilate.sh ``` -The installer links the version-controlled Claude Code settings to -`~/.claude/settings.json`. It also shares `agent-instructions.md` with Claude -Code and Codex by linking it to `~/.claude/CLAUDE.md` and -`~/.codex/AGENTS.md`. +The installer shares `agent-instructions.md` with Claude Code and Codex by +linking it to `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md`. + +### Codex configuration + +`assimilate.sh` uses `yq` to structurally merge the portable root settings from +`codex/config.managed.toml` into `~/.codex/config.toml`. It validates the merged +file with Codex's strict config loader before replacing the live config +atomically. Settings outside the managed fragment are preserved, and the first +existing config is retained as `~/.codex/config.toml.dotfiles-backup`. + +The checked-in settings enable turn notifications, automatic review, low +verbosity, concise reasoning summaries, and the pragmatic personality. Codex +uses `AGENTS.md` natively and falls back to `CLAUDE.md` when it is absent. + +Enable continuous Claude Code import once in the Codex UI and leave session +imports disabled. Those choices are desktop-local state and are deliberately +not automated through internal state files or JSON-RPC APIs. + +The portable Git preferences—branch prefix, squash merge method, and PR and +commit instructions—are managed in the schema-supported `[desktop]` table. +Other desktop preferences such as appearance, editor target, and queue behavior +remain local. Generated and machine-specific state such as local paths, +installed plugins, connector authentication, caches, session history, and +per-project trust also remains local. Never check in +`.codex-global-state.json`, Codex session databases, or the complete live +`config.toml`. + +### Claude Code configuration + +`assimilate.sh` uses `jq` to structurally merge the portable settings from +`claude/settings.managed.json` into `~/.claude/settings.json`. Local settings +such as permissions, hooks, plugins, and model selection are preserved. The +managed fragment enables concise output, configures the checked-in statusline, +and disables commit attribution, PR attribution, and cloud or Remote Control +session links. The first existing settings file is retained as +`~/.claude/settings.json.dotfiles-backup`. ### Tmux Plugin Manager - Install Tmux Plugin Manager ([Github](https://github.com/tmux-plugins/tpm#tmux-plugin-manager)) diff --git a/assimilate.sh b/assimilate.sh index 925af45..336b923 100755 --- a/assimilate.sh +++ b/assimilate.sh @@ -44,13 +44,15 @@ sym ghostty/config Library/Application\ Support/com.mitchellh.ghostty/confi sym tmux-powerline/config.sh .config/tmux-powerline/config.sh sym tmux-powerline/themes/theme.sh .config/tmux-powerline/themes/theme.sh sym claude/statusline.sh .claude/statusline.sh -sym claude/settings.json .claude/settings.json sym agent-instructions.md .claude/CLAUDE.md sym agent-instructions.md .codex/AGENTS.md sym zed/keymap.json .config/zed/keymap.json brew bundle install +"$DOTFILES/claude/sync-settings.sh" +"$DOTFILES/codex/sync-config.sh" + function clone_pinned () { url="$1"; dir="$2"; sha="$3" if [ ! -d "$dir" ]; then diff --git a/claude/settings.json b/claude/settings.json deleted file mode 100644 index e0f7602..0000000 --- a/claude/settings.json +++ /dev/null @@ -1,156 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/claude-code-settings.json", - "cleanupPeriodDays": 365, - "attribution": { - "commit": "", - "pr": "", - "sessionUrl": false - }, - "permissions": { - "allow": [ - "Bash(git fetch *)", - "Bash(ls *)", - "Bash(xargs *)", - "Bash(git mv *)", - "Bash(for chain in axelar boba corn monad nova memecore ronin multiversx linea algorand helium hivemapper hedera near jupiter walrus momentum flare metis iotex manta ink soneium astar janus_henderson moonbeam berachain unichain worldchain sonic abstract bob b3 mode apechain plume story codex hyperevm ripple movement katana conflux decentraland core plasma tempo_andantino provenance tezos)", - "Bash(if [ -d \"/Users/alexkan/work/gokustats-back-end/artemis_dagster/extract_load/primary_sources/$chain\" ])", - "Bash(then)", - "Bash(echo *)", - "Bash(for chain in axelar memecore ronin multiversx linea algorand helium hivemapper hedera near jupiter walrus momentum flare metis iotex manta ink soneium astar moonbeam berachain unichain worldchain sonic abstract bob b3 mode apechain plume story codex hyperevm ripple conflux core provenance)", - "Bash(gh pr list *)", - "Bash(git *)", - "Bash(gh *)", - "Bash(cat *)", - "Bash(head *)", - "Bash(tail *)", - "Bash(wc *)", - "Bash(which *)", - "Bash(pwd *)", - "Bash(find *)", - "Bash(grep *)", - "Bash(mkdir *)", - "Bash(touch *)", - "Bash(diff *)", - "Bash(chmod *)", - "Bash(cp *)", - "Bash(mv *)", - "Bash(curl *)", - "Bash(lsof *)", - "Bash(test *)", - "Read", - "Write", - "Edit", - "Grep", - "Glob", - "WebFetch", - "WebSearch", - "Skill(session-handoff)", - "Skill(session-handoff:*)", - "Skill(context-resume)", - "Skill(context-resume:*)", - "Skill(tech-debt)", - "Skill(tech-debt:*)", - "Skill(security-audit)", - "Skill(security-audit:*)", - "Skill(product-enhance)", - "Skill(product-enhance:*)", - "Skill(docs-review)", - "Skill(docs-review:*)", - "Skill(frontend-performance-audit)", - "Skill(frontend-performance-audit:*)", - "Edit(/tmp/**)", - "Edit(~/.claude/**)" - ], - "defaultMode": "auto" - }, - "model": "opus[1m]", - "outputStyle": "Concise", - "hooks": { - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "afplay /System/Library/Sounds/Glass.aiff" - }, - { - "type": "command", - "command": "INFO=$(tmux display-message -p 'Window #I, Pane #P' 2>/dev/null || echo '?'); osascript -e \"display notification \\\"Claude is ready! ($INFO)\\\" with title \\\"Claude Code\\\"\"" - } - ] - } - ], - "PreToolUse": [ - { - "matcher": "Bash", - "hooks": [ - { - "type": "command", - "command": "~/.claude/hooks/pretooluse-permissions.sh" - } - ] - } - ] - }, - "statusLine": { - "type": "command", - "command": "~/.claude/statusline.sh", - "padding": 0 - }, - "enabledPlugins": { - "pyright-lsp@claude-plugins-official": true, - "github@claude-plugins-official": true, - "code-review@claude-plugins-official": true, - "frontend-design@claude-plugins-official": true, - "security-guidance@claude-plugins-official": true, - "playwright@claude-plugins-official": true, - "typescript-lsp@claude-plugins-official": true, - "rust-analyzer-lsp@claude-plugins-official": true, - "lua-lsp@claude-plugins-official": true, - "pr-review-toolkit@claude-plugins-official": true, - "ralph-wiggum@claude-plugins-official": true, - "superpowers@claude-plugins-official": true, - "skill-creator@claude-plugins-official": true, - "telegram@claude-plugins-official": true, - "posthog@claude-plugins-official": true, - "planetscale@planetscale": true, - "artemis-skills@artemis-skills": true - }, - "extraKnownMarketplaces": { - "claude-plugins-official": { - "source": { - "source": "github", - "repo": "anthropics/claude-plugins-official" - } - }, - "planetscale": { - "source": { - "source": "github", - "repo": "planetscale/claude-plugin" - } - }, - "artemis-skills": { - "source": { - "source": "github", - "repo": "Artemis-xyz/artemis-skills" - } - } - }, - "sandbox": { - "enabled": true, - "autoAllowBashIfSandboxed": true, - "allowUnsandboxedCommands": true, - "excludedCommands": [] - }, - "alwaysThinkingEnabled": true, - "effortLevel": "high", - "skipDangerousModePermissionPrompt": true, - "skipWorkflowUsageWarning": true, - "editorMode": "vim", - "verbose": false, - "preferredNotifChannel": "ghostty", - "teammateMode": "auto", - "agentPushNotifEnabled": true, - "skipAutoPermissionPrompt": true, - "voiceEnabled": true -} diff --git a/claude/settings.managed.json b/claude/settings.managed.json new file mode 100644 index 0000000..b94cf63 --- /dev/null +++ b/claude/settings.managed.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "attribution": { + "commit": "", + "pr": "", + "sessionUrl": false + }, + "outputStyle": "Concise", + "statusLine": { + "type": "command", + "command": "~/.claude/statusline.sh", + "padding": 0 + } +} diff --git a/claude/sync-settings.sh b/claude/sync-settings.sh new file mode 100755 index 0000000..26a832f --- /dev/null +++ b/claude/sync-settings.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env sh + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +managed_settings="$script_dir/settings.managed.json" +claude_home=${CLAUDE_CONFIG_DIR:-"$HOME/.claude"} +dest_settings="$claude_home/settings.json" +backup_settings="$claude_home/settings.json.dotfiles-backup" + +if ! command -v jq >/dev/null 2>&1; then + echo "error: jq is required to sync Claude settings" >&2 + exit 1 +fi + +mkdir -p "$claude_home" +tmp_settings=$(mktemp "$claude_home/.settings.json.XXXXXX") + +cleanup() { + rm -f "$tmp_settings" +} +trap cleanup EXIT HUP INT TERM + +if [ -f "$dest_settings" ]; then + jq -s '.[0] * .[1]' "$dest_settings" "$managed_settings" > "$tmp_settings" +else + jq '.' "$managed_settings" > "$tmp_settings" +fi + +jq -e 'type == "object"' "$tmp_settings" >/dev/null +chmod 600 "$tmp_settings" + +if [ -f "$dest_settings" ] && [ ! -f "$backup_settings" ]; then + cp -p "$dest_settings" "$backup_settings" + chmod 600 "$backup_settings" +fi + +mv "$tmp_settings" "$dest_settings" +tmp_settings= + +echo "> Synced managed Claude settings to $dest_settings" diff --git a/codex/config.managed.toml b/codex/config.managed.toml new file mode 100644 index 0000000..2648f26 --- /dev/null +++ b/codex/config.managed.toml @@ -0,0 +1,23 @@ +#:schema https://developers.openai.com/codex/config-schema.json + +notify = ["/bin/sh", "-c", "/usr/bin/osascript -e 'display notification \"Turn completed\" with title \"Codex\"' >/dev/null 2>&1"] +model_reasoning_summary = "concise" +model_verbosity = "low" +personality = "pragmatic" +approvals_reviewer = "auto_review" + +# AGENTS.md is built in; use CLAUDE.md when no AGENTS.md is present. +project_doc_fallback_filenames = ["CLAUDE.md"] +project_doc_max_bytes = 196608 + +[desktop] +git-branch-prefix = "akan72/" +git-pull-request-merge-method = "squash" +git-commit-instructions = "Commit messages should be concise, just one sentence" +git-pr-instructions = """Create pull requests with the simple format + +## Summary + + +## Test Plan +""" diff --git a/codex/sync-config.sh b/codex/sync-config.sh new file mode 100755 index 0000000..5423ec0 --- /dev/null +++ b/codex/sync-config.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env sh + +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +managed_config="$script_dir/config.managed.toml" +codex_home=${CODEX_HOME:-"$HOME/.codex"} +dest_config="$codex_home/config.toml" +backup_config="$codex_home/config.toml.dotfiles-backup" + +if ! command -v yq >/dev/null 2>&1; then + echo "error: yq is required to sync Codex settings" >&2 + exit 1 +fi + +if ! command -v codex >/dev/null 2>&1; then + echo "error: codex is required to validate Codex settings" >&2 + exit 1 +fi + +mkdir -p "$codex_home" +tmp_config=$(mktemp "$codex_home/.config.toml.XXXXXX") +validation_home=$(mktemp -d "${TMPDIR:-/tmp}/codex-config-validation.XXXXXX") + +cleanup() { + rm -f "$tmp_config" + rm -rf "$validation_home" +} +trap cleanup EXIT HUP INT TERM + +if [ -f "$dest_config" ]; then + yq eval-all \ + --input-format=toml \ + --output-format=toml \ + 'select(fileIndex == 0) * select(fileIndex == 1)' \ + "$dest_config" "$managed_config" > "$tmp_config" +else + cp "$managed_config" "$tmp_config" +fi + +chmod 600 "$tmp_config" +cp "$tmp_config" "$validation_home/config.toml" +validation_log="$validation_home/stderr.log" +if ! CODEX_HOME="$validation_home" \ + codex app-server --strict-config --listen stdio:// \ + /dev/null 2>"$validation_log"; then + cat "$validation_log" >&2 + exit 1 +fi + +if [ -f "$dest_config" ] && [ ! -f "$backup_config" ]; then + cp -p "$dest_config" "$backup_config" + chmod 600 "$backup_config" +fi + +mv "$tmp_config" "$dest_config" +tmp_config= + +echo "> Synced managed Codex settings to $dest_config" diff --git a/tests/assimilate-path.sh b/tests/assimilate-path.sh new file mode 100755 index 0000000..1a222aa --- /dev/null +++ b/tests/assimilate-path.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env sh + +set -eu + +test_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH= cd -- "$test_dir/.." && pwd) +test_home=$(mktemp -d "${TMPDIR:-/tmp}/assimilate-path.XXXXXX") +codex_marker="$test_home/codex-called" + +cleanup() { + rm -rf "$test_home" +} +trap cleanup EXIT HUP INT TERM + +set +e +HOME="$test_home" \ + PATH="$test_dir/fixtures/bin:$PATH" \ + TEST_CODEX_MARKER="$codex_marker" \ + sh "$repo_root/assimilate.sh" >/dev/null 2>&1 +status=$? +set -e + +if [ "$status" -ne 1 ]; then + echo "expected assimilate.sh to reach the stubbed codex command, got status $status" >&2 + exit 1 +fi + +if [ ! -f "$codex_marker" ]; then + echo "expected the checkout's Codex sync script to invoke codex" >&2 + exit 1 +fi + +test "$(readlink "$test_home/.claude/CLAUDE.md")" = "$repo_root/agent-instructions.md" +test "$(readlink "$test_home/.codex/AGENTS.md")" = "$repo_root/agent-instructions.md" +test "$(readlink "$test_home/.claude/statusline.sh")" = "$repo_root/claude/statusline.sh" +test -f "$test_home/.claude/settings.json" +test ! -L "$test_home/.claude/settings.json" +test "$(jq -r '.outputStyle' "$test_home/.claude/settings.json")" = "Concise" + +expected_args='app-server --strict-config --listen stdio://' +actual_args=$(sed -n '1p' "$codex_marker") +if [ "$actual_args" != "$expected_args" ]; then + echo "expected Codex validator arguments $expected_args, got $actual_args" >&2 + exit 1 +fi + +echo "assimilate.sh resolves files relative to its own checkout" diff --git a/tests/claude-sync-settings.sh b/tests/claude-sync-settings.sh new file mode 100755 index 0000000..6822d34 --- /dev/null +++ b/tests/claude-sync-settings.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env sh + +set -eu + +test_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH= cd -- "$test_dir/.." && pwd) +test_root=$(mktemp -d "${TMPDIR:-/tmp}/claude-sync.XXXXXX") +claude_home="$test_root/claude-home" +invalid_home="$test_root/invalid-home" + +cleanup() { + rm -rf "$test_root" +} +trap cleanup EXIT HUP INT TERM + +mkdir -p "$claude_home" "$invalid_home" +cp "$test_dir/fixtures/claude/settings.json" "$claude_home/settings.json" +cp "$test_dir/fixtures/claude/invalid-settings.json" "$invalid_home/settings.json" + +original_hash=$(shasum -a 256 "$claude_home/settings.json" | awk '{print $1}') +CLAUDE_CONFIG_DIR="$claude_home" "$repo_root/claude/sync-settings.sh" + +settings="$claude_home/settings.json" +backup="$claude_home/settings.json.dotfiles-backup" +managed="$repo_root/claude/settings.managed.json" + +test "$(jq -r '.attribution.commit' "$settings")" = "" +test "$(jq -r '.attribution.pr' "$settings")" = "" +test "$(jq -r '.attribution.sessionUrl' "$settings")" = "false" +test "$(jq -r '.outputStyle' "$settings")" = "Concise" +test "$(jq -r '.statusLine.type' "$settings")" = "command" +test "$(jq -r '.statusLine.command' "$settings")" = "~/.claude/statusline.sh" +test "$(jq -r '.statusLine.padding' "$settings")" = "0" +test "$(jq -r '.model' "$settings")" = "keep-me" +test "$(jq -r '.enabledPlugins.local' "$settings")" = "true" +test "$(jq -r 'has("permissions")' "$managed")" = "false" +test "$(jq -r 'has("hooks")' "$managed")" = "false" +test "$(jq -r 'has("enabledPlugins")' "$managed")" = "false" +test "$(stat -f '%Lp' "$settings")" = "600" +test -f "$backup" +test "$(shasum -a 256 "$backup" | awk '{print $1}')" = "$original_hash" + +first_hash=$(shasum -a 256 "$settings" | awk '{print $1}') +CLAUDE_CONFIG_DIR="$claude_home" "$repo_root/claude/sync-settings.sh" +second_hash=$(shasum -a 256 "$settings" | awk '{print $1}') +test "$first_hash" = "$second_hash" +test "$(shasum -a 256 "$backup" | awk '{print $1}')" = "$original_hash" + +invalid_hash=$(shasum -a 256 "$invalid_home/settings.json" | awk '{print $1}') +set +e +CLAUDE_CONFIG_DIR="$invalid_home" "$repo_root/claude/sync-settings.sh" >/dev/null 2>&1 +status=$? +set -e +test "$status" -ne 0 +test "$(shasum -a 256 "$invalid_home/settings.json" | awk '{print $1}')" = "$invalid_hash" + +echo "Claude settings sync applies portable defaults and preserves local settings" diff --git a/tests/codex-sync-config.sh b/tests/codex-sync-config.sh new file mode 100755 index 0000000..b0d3910 --- /dev/null +++ b/tests/codex-sync-config.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env sh + +set -eu + +test_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +repo_root=$(CDPATH= cd -- "$test_dir/.." && pwd) +test_root=$(mktemp -d "${TMPDIR:-/tmp}/codex-sync.XXXXXX") +codex_home="$test_root/codex-home" +invalid_home="$test_root/invalid-home" + +cleanup() { + rm -rf "$test_root" +} +trap cleanup EXIT HUP INT TERM + +mkdir -p "$codex_home" "$invalid_home" +cp "$test_dir/fixtures/codex/config.toml" "$codex_home/config.toml" +cp "$test_dir/fixtures/codex/invalid-config.toml" "$invalid_home/config.toml" + +original_hash=$(shasum -a 256 "$codex_home/config.toml" | awk '{print $1}') +CODEX_HOME="$codex_home" "$repo_root/codex/sync-config.sh" + +config="$codex_home/config.toml" +backup="$codex_home/config.toml.dotfiles-backup" + +assert_value() { + expression=$1 + expected=$2 + actual=$(yq --input-format=toml --output-format=yaml --unwrapScalar "$expression" "$config") + if [ "$actual" != "$expected" ]; then + echo "expected $expression to be $expected, got $actual" >&2 + exit 1 + fi +} + +assert_value '.model' 'keep-me' +assert_value '.model_verbosity' 'low' +assert_value '.model_reasoning_summary' 'concise' +assert_value '.personality' 'pragmatic' +assert_value '.approvals_reviewer' 'auto_review' +assert_value '.project_doc_fallback_filenames[0]' 'CLAUDE.md' +assert_value '.project_doc_max_bytes' '196608' +assert_value '.notify[0]' '/bin/sh' +assert_value '.desktop."git-branch-prefix"' 'akan72/' +assert_value '.desktop."git-pull-request-merge-method"' 'squash' +assert_value '.desktop."git-commit-instructions"' 'Commit messages should be concise, just one sentence' +assert_value '.desktop."git-pr-instructions" | contains("## Summary")' 'true' +assert_value '.desktop."ambient-suggestions-enabled"' 'false' +assert_value '.projects."/tmp/example".trust_level' 'trusted' +assert_value '.mcp_servers.local.command' 'true' + +test "$(stat -f '%Lp' "$config")" = "600" +test -f "$backup" +test "$(shasum -a 256 "$backup" | awk '{print $1}')" = "$original_hash" + +first_hash=$(shasum -a 256 "$config" | awk '{print $1}') +CODEX_HOME="$codex_home" "$repo_root/codex/sync-config.sh" +second_hash=$(shasum -a 256 "$config" | awk '{print $1}') +test "$first_hash" = "$second_hash" +test "$(shasum -a 256 "$backup" | awk '{print $1}')" = "$original_hash" + +invalid_hash=$(shasum -a 256 "$invalid_home/config.toml" | awk '{print $1}') +set +e +CODEX_HOME="$invalid_home" "$repo_root/codex/sync-config.sh" >/dev/null 2>&1 +status=$? +set -e +test "$status" -ne 0 +test "$(shasum -a 256 "$invalid_home/config.toml" | awk '{print $1}')" = "$invalid_hash" + +echo "Codex config sync preserves local settings and applies managed values" diff --git a/tests/fixtures/bin/brew b/tests/fixtures/bin/brew new file mode 100755 index 0000000..3e156ec --- /dev/null +++ b/tests/fixtures/bin/brew @@ -0,0 +1,3 @@ +#!/usr/bin/env sh + +exit 0 diff --git a/tests/fixtures/bin/codex b/tests/fixtures/bin/codex new file mode 100755 index 0000000..bc3480a --- /dev/null +++ b/tests/fixtures/bin/codex @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +printf '%s\n' "$*" > "$TEST_CODEX_MARKER" +exit 1 diff --git a/tests/fixtures/claude/invalid-settings.json b/tests/fixtures/claude/invalid-settings.json new file mode 100644 index 0000000..cbad912 --- /dev/null +++ b/tests/fixtures/claude/invalid-settings.json @@ -0,0 +1,3 @@ +{ + "invalid": +} diff --git a/tests/fixtures/claude/settings.json b/tests/fixtures/claude/settings.json new file mode 100644 index 0000000..ac760d4 --- /dev/null +++ b/tests/fixtures/claude/settings.json @@ -0,0 +1,10 @@ +{ + "model": "keep-me", + "attribution": { + "commit": "old commit attribution", + "pr": "old PR attribution" + }, + "enabledPlugins": { + "local": true + } +} diff --git a/tests/fixtures/codex/config.toml b/tests/fixtures/codex/config.toml new file mode 100644 index 0000000..95d9847 --- /dev/null +++ b/tests/fixtures/codex/config.toml @@ -0,0 +1,12 @@ +model = "keep-me" +model_verbosity = "high" + +[desktop] +git-branch-prefix = "local/" +ambient-suggestions-enabled = false + +[projects."/tmp/example"] +trust_level = "trusted" + +[mcp_servers.local] +command = "true" diff --git a/tests/fixtures/codex/invalid-config.toml b/tests/fixtures/codex/invalid-config.toml new file mode 100644 index 0000000..51cccb1 --- /dev/null +++ b/tests/fixtures/codex/invalid-config.toml @@ -0,0 +1 @@ +not_a_real_codex_setting = true