Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Brewfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,44 @@ 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`.

### 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))
- Install tmux packages with `prefix + I`
19 changes: 19 additions & 0 deletions agent-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Commit Instructions

Commit messages should be concise, just one sentence

# Pull Request Instructions

Create pull requests with the simple format

## Summary
<Describe the context for your change>

## Test Plan
<Evidence that the PR is working, include screenshots, and the CLI commands that you ran>

# 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
7 changes: 6 additions & 1 deletion assimilate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
set -ex

PREFIX="$HOME"
DOTFILES="$PREFIX/dotfiles"
DOTFILES=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
BACKUPS="$PREFIX/backups"

function sym () {
Expand Down Expand Up @@ -44,10 +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 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
Expand Down
14 changes: 14 additions & 0 deletions claude/settings.managed.json
Original file line number Diff line number Diff line change
@@ -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
}
}
41 changes: 41 additions & 0 deletions claude/sync-settings.sh
Original file line number Diff line number Diff line change
@@ -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"
23 changes: 23 additions & 0 deletions codex/config.managed.toml
Original file line number Diff line number Diff line change
@@ -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
<Describe the context for your change>

## Test Plan
<Evidence that the PR is working, include screenshots, and the CLI commands that you ran>"""
59 changes: 59 additions & 0 deletions codex/sync-config.sh
Original file line number Diff line number Diff line change
@@ -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 >/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"
47 changes: 47 additions & 0 deletions tests/assimilate-path.sh
Original file line number Diff line number Diff line change
@@ -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"
57 changes: 57 additions & 0 deletions tests/claude-sync-settings.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading