When to invoke - User says "sync dotfiles", "pull configs", "push configs", "check drift" - After a session where permissions or hooks were modified - On a new machine after cloning ai-env Subcommands Map user intent to sync.sh subcommand: | Intent | Command | What it does | |--------|---------|-------------| | "check what's different" | | Show drift between repo and home | | "what's tracked" | | Show file registry | | "capture my local changes" | | Home → repo (home wins) | | "deploy repo configs" | | Repo → home (repo wins) | | "install skills" | | Install global skills from dotfiles/skills…

, stripped)\n if m:\n current_section = m.group(1)\n if current_section not in sections:\n sections[current_section] = OrderedDict()\n section_comments[current_section] = pending_comments\n pending_comments = []\n continue\n\n # Key = value\n m = re.match(r'^(\\w+)\\s*=\\s*(.+)

When to invoke - User says "sync dotfiles", "pull configs", "push configs", "check drift" - After a session where permissions or hooks were modified - On a new machine after cloning ai-env Subcommands Map user intent to sync.sh subcommand: | Intent | Command | What it does | |--------|---------|-------------| | "check what's different" | | Show drift between repo and home | | "what's tracked" | | Show file registry | | "capture my local changes" | | Home → repo (home wins) | | "deploy repo configs" | | Repo → home (repo wins) | | "install skills" | | Install global skills from dotfiles/skills…

, stripped)\n if m:\n key, val = m.group(1), m.group(2).strip()\n sect = current_section or \"__top__\"\n if sect not in sections:\n sections[sect] = OrderedDict()\n sections[sect][key] = val\n if pending_comments:\n if sect == \"__top__\" and sect not in section_comments:\n section_comments[sect] = pending_comments\n else:\n key_comments[(sect, key)] = pending_comments\n pending_comments = []\n continue\n\n if pending_comments:\n section_comments[\"__trailing__\"] = pending_comments\n return sections, section_comments, key_comments\n\ndef merge_configs(src_path, dst_path, out_path):\n src, src_sec_cmt, src_key_cmt = parse_toml_simple(src_path)\n dst, dst_sec_cmt, dst_key_cmt = parse_toml_simple(dst_path)\n\n # Union sections: destination as base, source overlaid\n merged = OrderedDict()\n all_sections = list(OrderedDict.fromkeys(\n list(dst.keys()) + list(src.keys())\n ))\n\n for section in all_sections:\n s_vals = src.get(section, OrderedDict())\n d_vals = dst.get(section, OrderedDict())\n m = OrderedDict(d_vals)\n m.update(s_vals)\n merged[section] = m\n\n # Write output preserving comment placement from source, fallback to destination\n lines = []\n for section in merged:\n if section == \"__trailing__\":\n continue\n\n # Section-level comments (before [header] or before first top-level key)\n cmt = src_sec_cmt.get(section, dst_sec_cmt.get(section, []))\n lines.extend(cmt)\n\n if section != \"__top__\":\n lines.append(f\"[{section}]\")\n\n for k, v in merged[section].items():\n # Key-level comments (before this specific key)\n kcmt = src_key_cmt.get((section, k), dst_key_cmt.get((section, k), []))\n lines.extend(kcmt)\n lines.append(f\"{k} = {v}\")\n\n lines.append(\"\") # blank line after each section\n\n # Trailing comments\n lines.extend(src_sec_cmt.get(\"__trailing__\", dst_sec_cmt.get(\"__trailing__\", [])))\n\n # Normalize: collapse 3+ consecutive blank lines to 1\n normalized = []\n for line in lines:\n if line.strip() == \"\" and normalized and normalized[-1].strip() == \"\":\n continue\n normalized.append(line)\n\n with open(out_path, \"w\") as f:\n f.write(\"\\n\".join(normalized))\n if normalized and normalized[-1].strip() != \"\":\n f.write(\"\\n\")\n\nmerge_configs(sys.argv[1], sys.argv[2], sys.argv[3])\nPYEOF\n}\n\n# --- CLAUDE.md section merge ---\n# Union-merges ## sections. Source wins on collision (same heading). Preamble: source wins.\n# Uses awk — no external dependencies beyond bash builtins.\n# — Claude claude-sonnet-4-6: awk-based section merge.\nmerge_claude_md() {\n local source=\"$1\" target=\"$2\" output=\"$3\"\n\n # If target doesn't exist, just copy source\n if [[ ! -f \"$target\" ]]; then\n cp \"$source\" \"$output\"\n return 0\n fi\n\n # If source doesn't exist, keep target\n if [[ ! -f \"$source\" ]]; then\n cp \"$target\" \"$output\"\n return 0\n fi\n\n # We parse each file into named sections keyed by \"## Heading\" text.\n # All sections from both sides are emitted (union). Source wins on collision.\n awk '\n BEGIN {\n src = ARGV[1]\n dst = ARGV[2]\n out = ARGV[3]\n\n # Parse source\n src_idx[0] = 0\n src_cur = \"__preamble__\"\n src_buf = \"\"\n while ((getline line \u003c src) > 0) {\n if (line ~ /^## /) {\n src_sections[src_cur] = src_buf\n if (!(src_cur in src_order_set)) {\n src_idx[0]++\n src_order[src_idx[0]] = src_cur\n src_order_set[src_cur] = 1\n }\n src_cur = line\n src_buf = line \"\\n\"\n } else {\n src_buf = src_buf line \"\\n\"\n }\n }\n close(src)\n src_sections[src_cur] = src_buf\n if (!(src_cur in src_order_set)) {\n src_idx[0]++\n src_order[src_idx[0]] = src_cur\n src_order_set[src_cur] = 1\n }\n\n # Parse destination\n dst_idx[0] = 0\n dst_cur = \"__preamble__\"\n dst_buf = \"\"\n while ((getline line \u003c dst) > 0) {\n if (line ~ /^## /) {\n dst_sections[dst_cur] = dst_buf\n if (!(dst_cur in dst_order_set)) {\n dst_idx[0]++\n dst_order[dst_idx[0]] = dst_cur\n dst_order_set[dst_cur] = 1\n }\n dst_cur = line\n dst_buf = line \"\\n\"\n } else {\n dst_buf = dst_buf line \"\\n\"\n }\n }\n close(dst)\n dst_sections[dst_cur] = dst_buf\n if (!(dst_cur in dst_order_set)) {\n dst_idx[0]++\n dst_order[dst_idx[0]] = dst_cur\n dst_order_set[dst_cur] = 1\n }\n\n # Build merged order: dst sections first, then src-only sections appended\n merged_idx = 0\n for (i = 1; i \u003c= dst_idx[0]; i++) {\n k = dst_order[i]\n merged_idx++\n merged_order[merged_idx] = k\n merged_set[k] = 1\n }\n for (i = 1; i \u003c= src_idx[0]; i++) {\n k = src_order[i]\n if (!(k in merged_set)) {\n merged_idx++\n merged_order[merged_idx] = k\n }\n }\n\n # Write output: source wins on collision (if key in src, use src; else use dst)\n for (i = 1; i \u003c= merged_idx; i++) {\n k = merged_order[i]\n if (k in src_sections) {\n printf \"%s\", src_sections[k] > out\n } else {\n printf \"%s\", dst_sections[k] > out\n }\n }\n close(out)\n }\n ' \"$source\" \"$target\" \"$output\"\n}\n\n# --- Dispatch: pick the right sync strategy ---\nsync_entry() {\n local src=\"$1\" dst=\"$2\" label=\"$3\" strategy=\"$4\"\n\n if [[ ! -f \"$src\" ]]; then\n echo -e \" ${YELLOW}skip${NC} $label (source missing)\"\n return\n fi\n\n case \"$strategy\" in\n settings)\n local tmp\n tmp=$(mktemp)\n if merge_settings_json \"$src\" \"$dst\" \"$tmp\"; then\n mkdir -p \"$(dirname \"$dst\")\"\n mv \"$tmp\" \"$dst\"\n echo -e \" ${GREEN}merge${NC} $label (semantic: permissions + hooks union)\"\n else\n rm -f \"$tmp\"\n echo -e \" ${RED}fail${NC} $label (merge failed, file unchanged)\"\n fi\n ;;\n codex_config)\n local tmp\n tmp=$(mktemp)\n if merge_codex_config \"$src\" \"$dst\" \"$tmp\"; then\n mkdir -p \"$(dirname \"$dst\")\"\n mv \"$tmp\" \"$dst\"\n echo -e \" ${GREEN}merge${NC} $label (semantic: profiles union, source wins scalars)\"\n else\n rm -f \"$tmp\"\n echo -e \" ${RED}fail${NC} $label (merge failed, file unchanged)\"\n fi\n ;;\n claude_md)\n local tmp\n tmp=$(mktemp)\n if merge_claude_md \"$src\" \"$dst\" \"$tmp\"; then\n mkdir -p \"$(dirname \"$dst\")\"\n mv \"$tmp\" \"$dst\"\n echo -e \" ${GREEN}merge${NC} $label (section merge: ## headings union, source wins collision)\"\n else\n rm -f \"$tmp\"\n echo -e \" ${RED}fail${NC} $label (merge failed, file unchanged)\"\n fi\n ;;\n copy)\n copy_file \"$src\" \"$dst\"\n echo -e \" ${GREEN}copy${NC} $label\"\n ;;\n *)\n echo -e \" ${RED}error${NC} $label (unknown strategy: $strategy)\"\n ;;\n esac\n}\n\n# --- Parse registry entry into path and strategy ---\nparse_entry() {\n local entry=\"$1\"\n FILE_PATH=\"${entry%%:*}\"\n FILE_STRATEGY=\"${entry##*:}\"\n}\n\n# --- Sync a single registry entry, expanding directory globs if path ends with \"/\" ---\n# Usage: sync_registry_entry \u003csrc_base> \u003cdst_base> \u003cfile_path> \u003cstrategy>\nsync_registry_entry() {\n local src_base=\"$1\" dst_base=\"$2\" file_path=\"$3\" strategy=\"$4\"\n\n if [[ \"$file_path\" == */ ]]; then\n # Directory glob: sync all .md files found in whichever side has the directory.\n # During push: iterate repo (src_base); during pull: iterate home (dst_base for pull = src).\n # We collect files from both sides to handle missing-in-one case gracefully.\n local dir=\"${file_path%/}\"\n local seen=()\n # Collect from src side\n if [[ -d \"$src_base/$dir\" ]]; then\n while IFS= read -r f; do\n local rel\n rel=\"$(basename \"$f\")\"\n seen+=(\"$rel\")\n done \u003c \u003c(find \"$src_base/$dir\" -maxdepth 1 -name \"*.md\" 2>/dev/null | sort)\n fi\n # Collect from dst side (files that may not be in src yet)\n if [[ -d \"$dst_base/$dir\" ]]; then\n while IFS= read -r f; do\n local rel\n rel=\"$(basename \"$f\")\"\n # Only add if not already seen\n local already=false\n for s in \"${seen[@]:-}\"; do [[ \"$s\" == \"$rel\" ]] && already=true && break; done\n [[ \"$already\" == false ]] && seen+=(\"$rel\")\n done \u003c \u003c(find \"$dst_base/$dir\" -maxdepth 1 -name \"*.md\" 2>/dev/null | sort)\n fi\n for rel in \"${seen[@]:-}\"; do\n if [[ -n \"$rel\" ]]; then\n sync_entry \"$src_base/$dir/$rel\" \"$dst_base/$dir/$rel\" \"$dir/$rel\" \"$strategy\"\n fi\n done\n else\n sync_entry \"$src_base/$file_path\" \"$dst_base/$file_path\" \"$file_path\" \"$strategy\"\n fi\n}\n\n# --- Expand a registry entry into concrete file paths (handles directory globs) ---\n# Populates EXPANDED_FILES array with relative paths.\nexpand_entry() {\n local src_base=\"$1\" dst_base=\"$2\" file_path=\"$3\"\n EXPANDED_FILES=()\n if [[ \"$file_path\" == */ ]]; then\n local dir=\"${file_path%/}\"\n local seen=()\n if [[ -d \"$src_base/$dir\" ]]; then\n while IFS= read -r f; do\n seen+=(\"$dir/$(basename \"$f\")\")\n done \u003c \u003c(find \"$src_base/$dir\" -maxdepth 1 -name \"*.md\" 2>/dev/null | sort)\n fi\n if [[ -d \"$dst_base/$dir\" ]]; then\n while IFS= read -r f; do\n local rel=\"$dir/$(basename \"$f\")\"\n local already=false\n for s in \"${seen[@]:-}\"; do [[ \"$s\" == \"$rel\" ]] && already=true && break; done\n [[ \"$already\" == false ]] && seen+=(\"$rel\")\n done \u003c \u003c(find \"$dst_base/$dir\" -maxdepth 1 -name \"*.md\" 2>/dev/null | sort)\n fi\n for rel in \"${seen[@]:-}\"; do\n if [[ -n \"$rel\" ]]; then\n EXPANDED_FILES+=(\"$rel\")\n fi\n done\n else\n EXPANDED_FILES=(\"$file_path\")\n fi\n}\n\n# --- Global skills install from lockfile ---\n# Reads dotfiles/skills-lock.json, groups by source, installs globally.\ninstall_global_skills_from_lockfile() {\n local lockfile=\"$AI_ENV_ROOT/dotfiles/skills-lock.json\"\n if [[ ! -f \"$lockfile\" ]]; then\n echo -e \" ${YELLOW}skip${NC} No dotfiles/skills-lock.json found\"\n return 0\n fi\n if ! command -v jq &>/dev/null; then\n echo -e \" ${YELLOW}skip${NC} jq required for skills install\"\n return 0\n fi\n if ! command -v npx &>/dev/null; then\n echo -e \" ${YELLOW}skip${NC} npx not found — skipping skills install\"\n return 0\n fi\n\n echo -e \"${CYAN}Installing global skills from dotfiles/skills-lock.json...${NC}\"\n local sources\n sources=$(jq -r '.skills | to_entries[] | .value.source' \"$lockfile\" | sort -u)\n if [[ -z \"$sources\" ]]; then\n echo -e \" ${YELLOW}skip${NC} No skills in lockfile\"\n return 0\n fi\n\n while IFS= read -r source; do\n [[ -n \"$source\" ]] || continue\n local skill_flags=''\n while IFS= read -r skill; do\n skill_flags=\"$skill_flags --skill $skill\"\n done \u003c \u003c(jq -r --arg src \"$source\" '.skills | to_entries[] | select(.value.source == $src) | .key' \"$lockfile\")\n if [[ -z \"$skill_flags\" ]]; then\n continue\n fi\n echo -e \" ${CYAN}source${NC} $source:$skill_flags\"\n if npx skills add \"$source\" $skill_flags -g -a claude-code -a codex -y \u003c/dev/null; then\n echo -e \" ${GREEN}ok${NC} Installed from $source\"\n else\n echo -e \" ${YELLOW}warn${NC} Install from $source failed (non-fatal)\"\n fi\n done \u003c\u003c\u003c \"$sources\"\n echo -e \"${GREEN}Done.${NC} Global skills installed.\"\n}\n\n# --- Commands ---\n\ncmd_push() {\n echo -e \"${CYAN}Installing global configs → home directory (repo wins on conflicts)${NC}\"\n echo \"\"\n\n echo \"Claude (~/.claude/):\"\n for entry in \"${CLAUDE_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n sync_registry_entry \"$CLAUDE_SRC\" \"$CLAUDE_DST\" \"$FILE_PATH\" \"$FILE_STRATEGY\"\n done\n\n echo \"\"\n echo \"Codex (~/.codex/):\"\n for entry in \"${CODEX_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n sync_registry_entry \"$CODEX_SRC\" \"$CODEX_DST\" \"$FILE_PATH\" \"$FILE_STRATEGY\"\n done\n\n # Install global skills from dotfiles/skills-lock.json\n echo \"\"\n install_global_skills_from_lockfile\n\n echo \"\"\n echo -e \"${GREEN}Done.${NC} Restart Claude Code / Codex to pick up changes.\"\n}\n\ncmd_skills_push() {\n install_global_skills_from_lockfile\n}\n\ncmd_pull() {\n # Block pull when AI_ENV_ROOT is a temporary clone — changes would be written to /tmp and lost.\n if [[ \"${AI_ENV_TEMP_CLONE:-false}\" == true ]]; then\n echo -e \"${RED}error${NC} Cannot pull to a temporary clone. Set AI_ENV_ROOT or clone ai-env locally.\" >&2\n echo \" Example: export AI_ENV_ROOT=~/projects/camacho/ai-env\" >&2\n exit 1\n fi\n\n echo -e \"${CYAN}Pulling user configs → repo (home wins on conflicts)${NC}\"\n echo \"\"\n\n echo \"Claude (~/.claude/ → dotfiles/claude/):\"\n for entry in \"${CLAUDE_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n sync_registry_entry \"$CLAUDE_DST\" \"$CLAUDE_SRC\" \"$FILE_PATH\" \"$FILE_STRATEGY\"\n done\n\n echo \"\"\n echo \"Codex (~/.codex/ → dotfiles/codex/):\"\n for entry in \"${CODEX_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n sync_registry_entry \"$CODEX_DST\" \"$CODEX_SRC\" \"$FILE_PATH\" \"$FILE_STRATEGY\"\n done\n\n echo \"\"\n echo -e \"${GREEN}Done.${NC} Review changes: git -C \\\"$AI_ENV_ROOT\\\" diff dotfiles/\"\n}\n\ncmd_diff() {\n echo -e \"${CYAN}Differences between repo and home directory${NC}\"\n echo \"\"\n local has_diff=false\n\n echo \"Claude:\"\n for entry in \"${CLAUDE_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n expand_entry \"$CLAUDE_SRC\" \"$CLAUDE_DST\" \"$FILE_PATH\"\n for f in \"${EXPANDED_FILES[@]:-}\"; do\n if [[ -z \"$f\" ]]; then continue; fi\n if [[ -f \"$CLAUDE_SRC/$f\" && -f \"$CLAUDE_DST/$f\" ]]; then\n if ! diff -q \"$CLAUDE_SRC/$f\" \"$CLAUDE_DST/$f\" >/dev/null 2>&1; then\n local strategy_label=\"copy\"\n [[ \"$FILE_STRATEGY\" == \"settings\" ]] && strategy_label=\"semantic merge\"\n [[ \"$FILE_STRATEGY\" == \"codex_config\" ]] && strategy_label=\"semantic merge\"\n [[ \"$FILE_STRATEGY\" == \"claude_md\" ]] && strategy_label=\"section merge\"\n echo -e \" ${YELLOW}changed${NC} $f (sync strategy: $strategy_label)\"\n $DIFF_CMD -u \"$CLAUDE_SRC/$f\" \"$CLAUDE_DST/$f\" 2>/dev/null | sed 's/^/ /' || true\n has_diff=true\n fi\n elif [[ -f \"$CLAUDE_SRC/$f\" ]]; then\n echo -e \" ${RED}missing in ~/.claude${NC} $f\"\n has_diff=true\n elif [[ -f \"$CLAUDE_DST/$f\" ]]; then\n echo -e \" ${RED}missing in repo${NC} $f\"\n has_diff=true\n fi\n done\n done\n\n echo \"\"\n echo \"Codex:\"\n for entry in \"${CODEX_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n expand_entry \"$CODEX_SRC\" \"$CODEX_DST\" \"$FILE_PATH\"\n for f in \"${EXPANDED_FILES[@]:-}\"; do\n if [[ -z \"$f\" ]]; then continue; fi\n if [[ -f \"$CODEX_SRC/$f\" && -f \"$CODEX_DST/$f\" ]]; then\n if ! diff -q \"$CODEX_SRC/$f\" \"$CODEX_DST/$f\" >/dev/null 2>&1; then\n echo -e \" ${YELLOW}changed${NC} $f (sync strategy: semantic merge)\"\n $DIFF_CMD -u \"$CODEX_SRC/$f\" \"$CODEX_DST/$f\" 2>/dev/null | sed 's/^/ /' || true\n has_diff=true\n fi\n elif [[ -f \"$CODEX_SRC/$f\" ]]; then\n echo -e \" ${RED}missing in ~/.codex${NC} $f\"\n has_diff=true\n elif [[ -f \"$CODEX_DST/$f\" ]]; then\n echo -e \" ${RED}missing in repo${NC} $f\"\n has_diff=true\n fi\n done\n done\n\n if [[ \"$has_diff\" = false ]]; then\n echo \"\"\n echo -e \"${GREEN}Everything in sync.${NC}\"\n fi\n}\n\ncmd_status() {\n echo -e \"${CYAN}Global config status${NC}\"\n echo \"\"\n\n printf \"%-50s %-8s %-8s %-16s\\n\" \"File\" \"Repo\" \"Home\" \"Merge Strategy\"\n printf \"%-50s %-8s %-8s %-16s\\n\" \"----\" \"----\" \"----\" \"--------------\"\n\n for entry in \"${CLAUDE_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n expand_entry \"$CLAUDE_SRC\" \"$CLAUDE_DST\" \"$FILE_PATH\"\n for f in \"${EXPANDED_FILES[@]:-}\"; do\n if [[ -z \"$f\" ]]; then continue; fi\n local repo_status=\"--\" home_status=\"--\"\n if [[ -f \"$CLAUDE_SRC/$f\" ]]; then repo_status=\"yes\"; fi\n if [[ -f \"$CLAUDE_DST/$f\" ]]; then home_status=\"yes\"; fi\n printf \"%-50s %-8s %-8s %-16s\\n\" \"claude/$f\" \"$repo_status\" \"$home_status\" \"$FILE_STRATEGY\"\n done\n done\n\n for entry in \"${CODEX_REGISTRY[@]}\"; do\n parse_entry \"$entry\"\n expand_entry \"$CODEX_SRC\" \"$CODEX_DST\" \"$FILE_PATH\"\n for f in \"${EXPANDED_FILES[@]:-}\"; do\n if [[ -z \"$f\" ]]; then continue; fi\n local repo_status=\"--\" home_status=\"--\"\n if [[ -f \"$CODEX_SRC/$f\" ]]; then repo_status=\"yes\"; fi\n if [[ -f \"$CODEX_DST/$f\" ]]; then home_status=\"yes\"; fi\n printf \"%-50s %-8s %-8s %-16s\\n\" \"codex/$f\" \"$repo_status\" \"$home_status\" \"$FILE_STRATEGY\"\n done\n done\n}\n\ncase \"${1:-}\" in\n push) cmd_push ;;\n pull) cmd_pull ;;\n diff) cmd_diff ;;\n status) cmd_status ;;\n skills-push) cmd_skills_push ;;\n *) usage ;;\nesac\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":25624,"content_sha256":"fd78a9c27f1b075d798c16ad001014ed09ba3d983b20c04a7ad277598c7fabc9"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":2},"content":[{"text":"When to invoke","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User says \"sync dotfiles\", \"pull configs\", \"push configs\", \"check drift\"","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"After a session where permissions or hooks were modified","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"On a new machine after cloning ai-env","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Subcommands","type":"text"}]},{"type":"paragraph","content":[{"text":"Map user intent to sync.sh subcommand:","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Intent","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Command","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"What it does","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"check what's different\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sync.sh diff","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Show drift between repo and home","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"what's tracked\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sync.sh status","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Show file registry","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"capture my local changes\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sync.sh pull","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Home → repo (home wins)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"deploy repo configs\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sync.sh push","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Repo → home (repo wins)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"install skills\"","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sync.sh skills-push","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Install global skills from dotfiles/skills-lock.json","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"\"sync\" (ambiguous)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Show diff first, then ask direction","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Safe default","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Running sync.sh","type":"text"}]},{"type":"paragraph","content":[{"text":"The sync script is bundled with this skill at ","type":"text"},{"text":"sync.sh","type":"text","marks":[{"type":"code_inline"}]},{"text":" (same directory as this SKILL.md). Run it directly — the script discovers the ai-env repo automatically:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"/path/to/skill/sync.sh \u003csubcommand>","type":"text"}]},{"type":"paragraph","content":[{"text":"The script sets ","type":"text"},{"text":"AI_ENV_ROOT","type":"text","marks":[{"type":"code_inline"}]},{"text":" automatically via the discovery chain below. To override:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"AI_ENV_ROOT=/custom/path/to/ai-env /path/to/skill/sync.sh push","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"AI_ENV_ROOT discovery","type":"text"}]},{"type":"paragraph","content":[{"text":"The script discovers the ai-env repo (where ","type":"text"},{"text":"dotfiles/","type":"text","marks":[{"type":"code_inline"}]},{"text":" lives) automatically:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"$AI_ENV_ROOT","type":"text","marks":[{"type":"code_inline"}]},{"text":" is set and ","type":"text"},{"text":"$AI_ENV_ROOT/dotfiles","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists: use that","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If the script is inside an ai-env git repo (e.g., running from a symlink): use that repo root","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"~/projects/camacho/ai-env","type":"text","marks":[{"type":"code_inline"}]},{"text":" exists: use that","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Clone ","type":"text"},{"text":"camacho/ai-env","type":"text","marks":[{"type":"code_inline"}]},{"text":" to a temp dir via GitHub","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If clone fails: error with instructions to set ","type":"text"},{"text":"AI_ENV_ROOT","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"After pull","type":"text"}]},{"type":"paragraph","content":[{"text":"Show ","type":"text"},{"text":"git diff dotfiles/","type":"text","marks":[{"type":"code_inline"}]},{"text":" and offer to commit: ","type":"text"},{"text":"chore: sync dotfiles","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"After push","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Run ","type":"text"},{"text":"sync.sh status","type":"text","marks":[{"type":"code_inline"}]},{"text":" to confirm, warn about any missing files.","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"sync-dotfiles","author":"@skillopedia","source":{"stars":1,"repo_name":"ai-skills","origin_url":"https://github.com/camacho/ai-skills/blob/HEAD/skills/sync-dotfiles/SKILL.md","repo_owner":"camacho","body_sha256":"820d311afd04d491754e2c6c72b6e8f79dc328d5c80381b747f7707451b1cc45","cluster_key":"baf1d69c14f94a6c3e12555f39537f47b858976385a6dd9e2d86d32a1972670e","clean_bundle":{"format":"clean-skill-bundle-v1","source":"camacho/ai-skills/skills/sync-dotfiles/SKILL.md","attachments":[{"id":"33851bff-a363-52b6-a832-e93e1d537b95","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/33851bff-a363-52b6-a832-e93e1d537b95/attachment.sh","path":"sync.sh","size":25624,"sha256":"fd78a9c27f1b075d798c16ad001014ed09ba3d983b20c04a7ad277598c7fabc9","contentType":"application/x-sh; charset=utf-8"}],"bundle_sha256":"4079cdd435675d41c7331cecc5537d02f553cd0e8a14c1df19f68261d52b01d5","attachment_count":1,"text_attachments":1,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":0,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"skills/sync-dotfiles/SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"software-engineering","category_label":"Engineering"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"software-engineering","import_tag":"clean-skills-v1","description":"Sync user-level AI configs (~/.claude/, ~/.codex/) with the dotfiles/ directory. Use when dotfiles drift, after sessions that change permissions/hooks, on new machines, or when the user says \"sync\", \"pull dotfiles\", \"push configs\"."}},"renderedAt":1782979543014}

When to invoke - User says "sync dotfiles", "pull configs", "push configs", "check drift" - After a session where permissions or hooks were modified - On a new machine after cloning ai-env Subcommands Map user intent to sync.sh subcommand: | Intent | Command | What it does | |--------|---------|-------------| | "check what's different" | | Show drift between repo and home | | "what's tracked" | | Show file registry | | "capture my local changes" | | Home → repo (home wins) | | "deploy repo configs" | | Repo → home (repo wins) | | "install skills" | | Install global skills from dotfiles/skills…