chore: no idea?

This commit is contained in:
Björn Fromme committed 2026-10-16 15:10:26 +02:00
1 parent 63f3199076
commit afc232ac27
8 files changed
+411 -18

No files matched your search

+112 -15
View File
@@ -7,11 +7,13 @@
- exposes `ddev codex`
- exposes `ddev claude`
- optionally exposes `ddev claude-swap` for juggling multiple Claude accounts (opt-in; see [Multiple Claude accounts](#multiple-claude-accounts))
- optionally keeps read-only mirrors of sibling repos under `.ddev/claude-code/home/refs/` for Claude to grep (opt-in; see [Sibling repo mirrors](#sibling-repo-mirrors))
- installs Codex CLI lazily the first time you run `ddev codex` (or whenever the persisted binary is missing) by downloading the latest release from GitHub: `curl -fsSL https://github.com/openai/codex/releases/latest/download/install.sh | sh`
- installs Claude Code CLI during `ddev start` when the persisted binary is missing or broken by downloading the stable native build: `curl -fsSL https://claude.ai/install.sh | bash -s stable`
- keeps normal `ddev start` fast by skipping Claude Code installation when the persisted binary is already runnable
- persists each CLI's binary, HOME, and XDG paths under **project-local** storage so each DDEV project has its own profiles
- seeds the project-local HOME from the container home on first bootstrap for `.gitignore_global` when present
- keeps its own artifacts out of the project's git via a managed block in `.git/info/exclude`, refreshed on every `ddev start` and `ddev claude` (see [Keeping add-on files out of git](#keeping-add-on-files-out-of-git))
- always runs Codex with `--sandbox danger-full-access --ask-for-approval untrusted`
## Runtime storage
@@ -74,6 +76,8 @@ Then restart:
ddev claude-swap list # opt-in; see "Multiple Claude accounts"
ddev claude-refs # opt-in; see "Sibling repo mirrors"
## Updates
Codex CLI is installed lazily when `ddev codex` runs and the persisted binary is missing or broken.
@@ -151,34 +155,127 @@ The first install needs outbound network access to `astral.sh`, `pypi.org`,
`claude-swap` captures whichever account is signed in at `cswap add` time, so the
initial multi-account setup is a few manual `/login` round-trips.
## Sibling repo mirrors
If a project works alongside sibling repos on the same self-hosted Git host, Claude can be given
read-only, auto-refreshing local clones of them to grep for prior art — plus, optionally, the
`gitea` MCP server for their issues and PRs. This is **opt-in per project** and driven entirely by
one manifest, `.ddev/claude-refs.txt`; projects without that file see no change at all.
`.ddev/claude-refs.txt` is **local by default**: the add-on's [managed git-exclude
block](#keeping-add-on-files-out-of-git) ignores it, since it typically names a LAN-only Git host,
a bot token, and mirrors specific to one person's sandbox. A project that wants the manifest shared
with the rest of the team can still track it with `git add -f .ddev/claude-refs.txt`.
### Manifest: `.ddev/claude-refs.txt`
```
# Settings: one per line, starting with @
@host git.example.com # required
@user claude-ro # bot account; with @token, drives git credentials
@token ~/.gitea-ro-token # default; 0600 file in the sandbox home, never committed
@depth 50 # default
@mcp gitea https://mcp.example.com/mcp # optional: name + URL, registered with the token as Bearer
# Mirrors: <dir-name> <owner/repo> <description, rest of line, becomes the README row>
sibling-api org/sibling-api Go service this project talks to over HTTP.
shared-lib org/shared-lib Shared component library, imported as a dependency.
```
Inline `#` comments are only allowed on setting lines, after all the values the setting reads.
Keep them off mirror lines, or they end up in the description.
Environment overrides, mainly for testing: `CLAUDE_REFS_MANIFEST`, `CLAUDE_REFS_HOME`,
`CLAUDE_REFS_MAX_AGE` (seconds), `CLAUDE_REFS_TIMEOUT` (seconds per clone or fetch), and
`CLAUDE_REFS_DETACHED` (set by `install.sh`; sends output to `refs/.sync.log`).
### What happens once a project has a manifest
- On `ddev start` and on every `ddev claude`, `install.sh` runs the sync in the background
(`setsid nohup`) so neither command waits on the network. A stamp file throttles real syncs to
at most one every 6h (`CLAUDE_REFS_MAX_AGE`), written only after a fully successful run, so a
failure retries next launch. If the Git host is unreachable, the sync logs a line and exits
cleanly rather than failing `ddev start`.
- Mirrors are shallow (`--depth 50 --single-branch` by default), live under
`.ddev/claude-code/home/refs/` (gitignored, survives `ddev restart`), and are **throwaway**:
every sync hard-resets them to the remote's current state, so local changes are lost and nothing
is ever merged. A directory that drops out of the manifest is left in place with a warning
rather than deleted.
- An entry whose repo resolves to the project's own `origin` is skipped, so the project never ends
up with a second, stale copy of itself.
- `refs/README.md` is regenerated from the manifest on every sync — descriptions come from the
manifest, so the table can't drift from what's on disk.
- The bot account's credential file and git config are rewritten from the manifest on every run,
so a renamed bot account heals itself on the next `ddev claude` instead of failing silently.
- Run `ddev claude-refs` to sync immediately instead of waiting for the throttle; inside a Claude
session, running `.ddev/claude-code/refs-sync.sh --force` does the same and is already on the
permission allowlist.
- If `@mcp` is set, the MCP server is registered once at user scope (`claude mcp add --scope
user`), and required permission-allowlist entries are added once — both idempotently, so re-runs
never duplicate them.
Access control (which repos the mirrors can see) stays entirely in the Git host: the bot account's
collaborator access decides what's visible. The manifest only names repos; it grants nothing by
itself.
### Adopting in a project
1. On the Git host, add the bot account as a **read-only** collaborator to each sibling repo.
2. Create `.ddev/claude-refs.txt` with `@host`, `@user`, and the mirrors you want. It stays local
unless you `git add -f` it (see above).
3. Put the bot's token at `.ddev/claude-code/home/.gitea-ro-token`, mode `0600` (matches the
manifest's default `@token ~/.gitea-ro-token`; never commit it).
4. `ddev restart`, then check `.ddev/claude-code/home/refs/.sync.log` after a minute or so.
5. Paste this into the project's `CLAUDE.md`, adding a sentence about the most relevant sibling:
```markdown
## Sibling repos
Read-only shallow clones of sibling projects live in `.ddev/claude-code/home/refs/`
(gitignored, declared in `.ddev/claude-refs.txt`). Grep them for prior art before
re-deriving a solution; `refs/README.md` says what each one is. They are reference
material: never commit or push from them. They sync in the background at most every
6h; run `ddev claude-refs` before trusting anything time-sensitive.
```
The container still needs to resolve and reach the Git host (and the MCP host, if used).
## Installed managed files
.ddev/config.codex-cli.yaml
.ddev/config.claude-code.yaml
.ddev/config.codex-claude.yaml
.ddev/commands/web/codex
.ddev/commands/web/claude
.ddev/commands/web/claude-swap
.ddev/commands/web/claude-refs
.ddev/codex-cli/install.sh
.ddev/claude-code/install.sh
.ddev/claude-code/install-swap.sh
.ddev/claude-code/refs-sync.sh
.ddev/claude-code/git-exclude.sh
## Notes
## Keeping add-on files out of git
Every artifact the add-on drops into a project — `.ddev/claude-code/`, `.ddev/codex-cli/`, the
`commands/web/*` wrappers, `.ddev/config.codex-claude.yaml`, `.ddev/addon-metadata/ddev-codex-claude-cli`,
`.ddev/plans`, and `.ddev/claude-refs.txt` — should stay out of the project's git history.
`.ddev/claude-code/git-exclude.sh` maintains a marker-delimited block for this in the project's
`.git/info/exclude` (not `.gitignore`): a per-clone file that every git process honours, including
git run from the host, regardless of `$HOME` or container config. `install.sh` runs it on every
`ddev start` and every `ddev claude` (and the codex installer runs the same script too), so the
block always matches the add-on's current file list — nothing to copy or keep in sync by hand.
Content outside the `>>> ddev-codex-claude-cli >>>` / `<<< ddev-codex-claude-cli <<<` markers is
left untouched; only the block between them is rewritten. Personal patterns (`.DS_Store`, `.idea`,
`CLAUDE.md`, …) are not part of this block — they belong in your own `~/.gitignore_global`, wired
up via `core.excludesfile` as described below, since whether a given project tracks its own
`CLAUDE.md` is not the add-on's call.
- if the container home has `.gitignore_global` (typically from host `~/.ddev/homeadditions/.gitignore_global`), it is copied into each project-local CLI `HOME` on first bootstrap; git's `core.excludesfile` is pointed at that project-local copy — this only affects git run *inside* the container, which is why the add-on's own artifacts use the `.git/info/exclude` block instead
- the first `ddev codex` in a project can take some time because it downloads the Codex CLI binary; later runs reuse `.ddev/codex-cli/bin/codex`
- the first `ddev start` after installing the add-on can take some time because it downloads Claude Code; later starts reuse `.ddev/claude-code/home/.local/bin/claude`
- if the container home has `.gitignore_global` (typically from host `~/.ddev/homeadditions/.gitignore_global`), it is copied into each project-local CLI `HOME` on first bootstrap; git's `core.excludesfile` is pointed at that project-local copy
- if you do not want add-on or CLI runtime files in your repository, ignore them in git, for example:
/.ddev/addon-metadata/ddev-codex-claude-cli
/.ddev/config.codex-cli.yaml
/.ddev/config.claude-code.yaml
/.ddev/commands/web/codex
/.ddev/commands/web/claude
/.ddev/commands/web/claude-swap
/.ddev/codex-cli/
/.ddev/claude-code/
Removing the add-on deletes `.ddev/codex-cli/` and `.ddev/claude-code/` as well (local auth, config, and CLI binaries). Back up those directories first if you need to keep them.
Removing the add-on deletes `.ddev/codex-cli/` and `.ddev/claude-code/` as well (local auth, config, and CLI binaries). Back up those directories first if you need to keep them. The `.git/info/exclude` block is left in place; an orphaned entry for a path that no longer exists is harmless.
## macOS / Linux / WSL2
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
#ddev-generated
# Keep the add-on's own artifacts out of git, in every project it is installed in.
#
# Rewrites a marker-delimited block in the project's .git/info/exclude. That file is
# per-clone and never committed, and every git process honours it -- unlike
# core.excludesfile, which is set on the container's git config only and so does
# nothing for git run from the host. Called by install.sh, which runs on "ddev start"
# and on every "ddev claude", so the list refreshes itself as the add-on changes.
#
# Personal patterns (.DS_Store, .idea, CLAUDE.md, ...) are deliberately NOT here:
# they belong in ~/.gitignore_global, not to this add-on.
set -uo pipefail
BEGIN='# >>> ddev-codex-claude-cli >>>'
END='# <<< ddev-codex-claude-cli <<<'
PATTERNS=(
'.ddev/addon-metadata/ddev-codex-claude-cli/'
'.ddev/config.codex-claude.yaml'
'.ddev/commands/web/claude'
'.ddev/commands/web/claude-swap'
'.ddev/commands/web/claude-refs'
'.ddev/commands/web/codex'
'.ddev/claude-code/'
'.ddev/codex-cli/'
'.ddev/claude-refs.txt'
'.ddev/plans/'
)
approot="${DDEV_APPROOT:-$(pwd)}"
# --git-common-dir resolves a worktree's .git file to the real directory. Anything
# that is not a git checkout is not an error: ddev start must never fail over this.
gitdir="$(git -C "${approot}" rev-parse --git-common-dir 2>/dev/null)" || exit 0
[ -n "${gitdir}" ] || exit 0
case "${gitdir}" in /*) ;; *) gitdir="${approot}/${gitdir}" ;; esac
[ -d "${gitdir}" ] || exit 0
info="${gitdir}/info"
exclude="${info}/exclude"
mkdir -p "${info}" 2>/dev/null || exit 0
[ -e "${exclude}" ] || : >"${exclude}" 2>/dev/null || exit 0
[ -w "${exclude}" ] || exit 0
block="$(
printf '%s\n' "${BEGIN}"
printf '%s\n' "# Managed by .ddev/claude-code/git-exclude.sh. Edits inside this block are lost."
printf '%s\n' "${PATTERNS[@]}"
printf '%s\n' "${END}"
)"
# Existing block replaced in place, otherwise appended after a blank line.
if grep -qF "${BEGIN}" "${exclude}" && grep -qF "${END}" "${exclude}"; then
updated="$(awk -v begin="${BEGIN}" -v end="${END}" -v block="${block}" '
index($0, begin) == 1 { print block; skip = 1; next }
skip && index($0, end) == 1 { skip = 0; next }
!skip { print }
' "${exclude}")"
else
updated="$(cat "${exclude}"; [ -s "${exclude}" ] && printf '\n'; printf '%s\n' "${block}")"
fi
# Only write on a real change, so "ddev start" does not touch the mtime every time.
# cat > rather than mv: the file may be bind-mounted.
if [ "${updated}" != "$(cat "${exclude}")" ]; then
printf '%s\n' "${updated}" >"${exclude}" || exit 0
fi
+16
View File
@@ -73,4 +73,20 @@ if command -v git >/dev/null 2>&1 && [ -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then
fi
fi
# Keep the add-on's own artifacts out of this project's git, via a managed block in
# .git/info/exclude. Runs every time so the list follows the add-on, not a file that
# was copied once. Never fatal: a project need not be a git checkout.
if [ -x "${PERSIST_BASE}/git-exclude.sh" ]; then
"${PERSIST_BASE}/git-exclude.sh" || true
fi
# Sibling-repo mirrors, opt-in via .ddev/claude-refs.txt. Setup is local and quick;
# the sync runs detached so neither ddev start nor ddev claude waits on the network,
# and refs-sync.sh throttles itself.
REFS_SYNC="${PERSIST_BASE}/refs-sync.sh"
if [ -f "${DDEV_APPROOT}/.ddev/claude-refs.txt" ] && [ -x "${REFS_SYNC}" ]; then
"${REFS_SYNC}" --setup-only || echo "claude-refs: setup failed, continuing" >&2
CLAUDE_REFS_DETACHED=1 setsid nohup "${REFS_SYNC}" >/dev/null 2>&1 </dev/null &
fi
echo "Claude Code version: $("${PERSIST_BIN}" --version)"
+195
View File
@@ -0,0 +1,195 @@
#!/usr/bin/env bash
#ddev-generated
# Read-only sibling-repo mirrors, declared in .ddev/claude-refs.txt.
#
# refs-sync.sh throttled sync (skips if the last good sync is recent)
# refs-sync.sh --force sync now
# refs-sync.sh --setup-only credentials, permissions and MCP registration; no network
#
# Mirrors are throwaway: existing ones are hard-reset, never merged. Does nothing
# at all when the project has no manifest.
set -uo pipefail
MANIFEST="${CLAUDE_REFS_MANIFEST:-${DDEV_APPROOT}/.ddev/claude-refs.txt}"
PERSIST_HOME="${CLAUDE_REFS_HOME:-${DDEV_APPROOT}/.ddev/claude-code/home}"
REFS="${PERSIST_HOME}/refs"
STAMP="${REFS}/.last-sync"
MAX_AGE="${CLAUDE_REFS_MAX_AGE:-21600}" # seconds; 6h
NET_TIMEOUT="${CLAUDE_REFS_TIMEOUT:-120}" # seconds per clone/fetch
SELF="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
[ -f "${MANIFEST}" ] || exit 0
MODE=sync
case "${1:-}" in
'') ;;
--force) MODE=force ;;
--setup-only) MODE=setup ;;
*) echo "usage: $(basename "$0") [--force|--setup-only]" >&2; exit 2 ;;
esac
export HOME="${PERSIST_HOME}"
export GIT_TERMINAL_PROMPT=0
# --- manifest ---------------------------------------------------------------
host=""; user=""; token="~/.gitea-ro-token"; depth=50; mcp_name=""; mcp_url=""
names=(); repos=(); descs=()
while IFS= read -r line || [ -n "${line}" ]; do
line="${line%$'\r'}"
case "${line}" in ''|'#'*) continue ;; esac
read -r key val rest <<<"${line}"
case "${key}" in
@host) host="${val}" ;;
@user) user="${val}" ;;
@token) token="${val}" ;;
@depth) depth="${val}" ;;
@mcp) mcp_name="${val}"; mcp_url="${rest}" ;;
@*) echo "claude-refs: unknown setting ${key}, ignored" >&2 ;;
*) names+=("${key}"); repos+=("${val}"); descs+=("${rest}") ;;
esac
done <"${MANIFEST}"
token="${token/#\~/${HOME}}"
if [ -z "${host}" ]; then
echo "claude-refs: no @host in ${MANIFEST}" >&2
exit 1
fi
# --- credentials (every run, so a renamed bot account heals itself) --------
# Host-scoped on purpose: a global helper would hand the project's own HTTPS
# pushes the bot's pull-only credentials.
creds="${HOME}/.git-credentials-${host}"
if [ -n "${user}" ] && [ -r "${token}" ]; then
(umask 077; printf 'https://%s:%s@%s\n' "${user}" "$(tr -d '[:space:]' <"${token}")" "${host}" >"${creds}")
git config --global "credential.https://${host}.helper" "store --file=${creds}"
git config --global "credential.https://${host}.username" "${user}"
elif [ -n "${user}" ]; then
echo "claude-refs: token ${token} not readable; relying on existing git credentials" >&2
fi
# --- setup: permissions and MCP (no network) --------------------------------
if [ "${MODE}" = setup ]; then
mkdir -p "${REFS}" "${HOME}/.claude"
if ! command -v jq >/dev/null 2>&1; then
echo "claude-refs: jq missing, skipping permission and MCP seeding" >&2
exit 0
fi
settings="${HOME}/.claude/settings.json"
[ -s "${settings}" ] || echo '{}' >"${settings}"
want=("Bash(git -C ${REFS}/*)" "Bash(${SELF})" "Bash(${SELF} --force)")
[ -n "${mcp_name}" ] && want+=("mcp__${mcp_name}")
tmp="$(mktemp)"
# With --args every trailing word is an argument, so the file comes in on stdin.
if jq '
.permissions.allow = reduce $ARGS.positional[] as $e
(.permissions.allow // []; if any(.[]; . == $e) then . else . + [$e] end)
' --args "${want[@]}" <"${settings}" >"${tmp}"; then
cat "${tmp}" >"${settings}"
fi
rm -f "${tmp}"
# Checked against ~/.claude.json rather than "claude mcp get", which connects.
if [ -n "${mcp_name}" ] && [ -n "${mcp_url}" ] && [ -r "${token}" ] \
&& ! jq -e --arg n "${mcp_name}" '.mcpServers[$n]' "${HOME}/.claude.json" >/dev/null 2>&1; then
claude mcp add --transport http --scope user "${mcp_name}" "${mcp_url}" \
--header "Authorization: Bearer $(tr -d '[:space:]' <"${token}")"
fi
exit 0
fi
# --- sync -------------------------------------------------------------------
mkdir -p "${REFS}"
exec 9>"${REFS}/.sync.lock"
if ! flock -n 9; then
echo "claude-refs: another sync is running"
exit 0
fi
if [ "${MODE}" = sync ] && [ -f "${STAMP}" ] \
&& [ $(( $(date +%s) - $(stat -c %Y "${STAMP}") )) -lt "${MAX_AGE}" ]; then
exit 0
fi
# Detached runs (ddev start, ddev claude) keep only the latest real sync's output.
[ -n "${CLAUDE_REFS_DETACHED:-}" ] && exec >"${REFS}/.sync.log" 2>&1
date '+claude-refs sync %F %T'
if ! curl -sS -o /dev/null --max-time 10 "https://${host}/" 2>/dev/null; then
echo "claude-refs: ${host} unreachable (off-LAN?), mirrors left as they are"
exit 0
fi
# git@h:o/r.git, https://u@h/o/r.git -> h/o/r (lowercase), to spot the project itself
normalize() {
local u="$1"
u="${u#*://}"; u="${u#*@}"; u="${u/://}"; u="${u%/}"; u="${u%.git}"
printf '%s' "${u,,}"
}
self="$(normalize "$(git -C "${DDEV_APPROOT}" remote get-url origin 2>/dev/null)")"
declare -A listed=()
rows=()
failed=0
for i in "${!names[@]}"; do
name="${names[$i]}"; repo="${repos[$i]}"; dir="${REFS}/${name}"
url="https://${host}/${repo}.git"
if [ -n "${self}" ] && [ "$(normalize "${url}")" = "${self}" ]; then
printf '== %-16s skipped: this is the project itself, read it live\n' "${name}"
continue
fi
listed[${name}]=1
rows+=("| \`${name}/\` | \`${repo}\` | ${descs[$i]//|/\\|} |")
printf '== %-16s ' "${name}"
if [ -d "${dir}/.git" ]; then
if ! timeout "${NET_TIMEOUT}" git -C "${dir}" fetch --depth "${depth}" --quiet origin \
|| ! git -C "${dir}" reset --hard --quiet '@{u}'; then
echo "FAILED (fetch)"; failed=1; continue
fi
elif [ -e "${dir}" ]; then
echo "FAILED (exists but is not a git checkout)"; failed=1; continue
elif ! timeout "${NET_TIMEOUT}" git clone --depth "${depth}" --single-branch --quiet "${url}" "${dir}"; then
rm -rf "${dir}" # did not exist before this run
echo "FAILED (clone)"; failed=1; continue
fi
git -C "${dir}" log -1 --format='%ad %s' --date=short
done
for dir in "${REFS}"/*/; do
[ -d "${dir}" ] || continue
name="$(basename "${dir}")"
[ -n "${listed[${name}]:-}" ] || echo "!! ${name}/ is not in the manifest; left in place, remove it by hand"
done
{
cat <<EOF
# Sibling repo mirrors
<!-- Generated by refs-sync.sh from .ddev/claude-refs.txt. Edit the manifest, not this file. -->
Read-only shallow clones of sibling projects on \`${host}\`, kept here so
cross-project questions ("how did we solve X over there?") can be answered by
grepping real code instead of guessing or re-deriving it.
**These are reference material, not working copies.** Never commit, never push,
never edit. The account behind them (\`${user:-unknown}\`) should have pull access only,
but treat them as read-only regardless. To change one of these projects, open
its real checkout.
Clones are \`--depth ${depth} --single-branch\` off the default branch. They sync
themselves in the background on \`ddev start\` and \`ddev claude\`, at most every
$(( MAX_AGE / 3600 ))h. Run \`${SELF} --force\` before trusting anything time-sensitive.
Syncing hard-resets, so local changes are lost. The last run's output is in \`.sync.log\`.
| Directory | Repo | What it is |
|---|---|---|
EOF
printf '%s\n' "${rows[@]}"
} >"${REFS}/README.md"
if [ "${failed}" -eq 0 ]; then
touch "${STAMP}"
fi
exit "${failed}"
+7
View File
@@ -71,4 +71,11 @@ if command -v git >/dev/null 2>&1 && [ -f "${PERSIST_GITIGNORE_GLOBAL}" ]; then
fi
fi
# Shared with the Claude side: keeps the add-on's artifacts out of the project's git
# even when only the codex installer runs.
GIT_EXCLUDE="${DDEV_APPROOT}/.ddev/claude-code/git-exclude.sh"
if [ -x "${GIT_EXCLUDE}" ]; then
"${GIT_EXCLUDE}" || true
fi
echo "Codex CLI version: $("${PERSIST_BIN}" --version)"
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
#ddev-generated
## Description: Sync the read-only sibling-repo mirrors declared in .ddev/claude-refs.txt now
## Usage: claude-refs
## Example: "ddev claude-refs"
set -euo pipefail
exec "${DDEV_APPROOT}/.ddev/claude-code/refs-sync.sh" --force
File renamed without changes.
+6 -3
View File
@@ -1,24 +1,27 @@
name: ddev-codex-claude-cli
project_files:
- config.codex-claude-cli.yaml
- config.codex-claude.yaml
- commands/web/codex
- commands/web/claude
- commands/web/claude-swap
- commands/web/claude-refs
- codex-cli/install.sh
- claude-code/install.sh
- claude-code/install-swap.sh
- claude-code/refs-sync.sh
- claude-code/git-exclude.sh
post_install_actions:
- |
#ddev-description: Make AI CLI scripts executable
set -eu
chmod +x commands/web/codex commands/web/claude commands/web/claude-swap codex-cli/install.sh claude-code/install.sh claude-code/install-swap.sh
chmod +x commands/web/codex commands/web/claude commands/web/claude-swap commands/web/claude-refs codex-cli/install.sh claude-code/install.sh claude-code/install-swap.sh claude-code/refs-sync.sh claude-code/git-exclude.sh
removal_actions:
- |
#ddev-description: Remove AI CLI add-on artifacts
rm -f config.codex-claude-cli.yaml commands/web/codex commands/web/claude commands/web/claude-swap 2>/dev/null || true
rm -f config.codex-claude.yaml commands/web/codex commands/web/claude commands/web/claude-swap commands/web/claude-refs 2>/dev/null || true
rm -rf codex-cli 2>/dev/null || true
rm -rf claude-code 2>/dev/null || true