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
47 changes: 40 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ want these operating rules to apply across sessions.
- Append `--doctor` to any wrapper to check the image, tool versions, Git
identity, PDF tooling, branch, and worktree status without launching Codex.

Named instances let you intentionally run more than one container for the same
checkout:

```sh
codexcli --instance review
codexcli --instance=review
CODEXCLI_INSTANCE=review codexcli
```

The `--instance` flag must appear before Codex arguments. `--instance NAME`
and `--instance=NAME` override `CODEXCLI_INSTANCE`. If a future Codex CLI
argument is named `--instance`, pass it after `--`, for example
`codexcli -- --instance value`.

All other positional arguments are passed unchanged to Codex.

## Repositories and worktrees
Expand All @@ -49,14 +63,33 @@ Run a wrapper from anywhere inside a Git checkout. The repository root is
found with `git rev-parse --show-toplevel`, so normal checkouts and linked Git
worktrees (whose `.git` is a file) are recognized. Only that root is mounted.

Container names combine the checkout basename with a short SHA-256 hash of its
full path. Separate worktrees therefore have stable, distinct names and can run
in parallel. A wrapper refuses to start when its exact container name already
exists; inspect or remove that container explicitly before retrying.
By default, container names combine the checkout basename with a short SHA-256
hash of its full path:

```text
codexcli-<workspace-slug>-<hash>
```

With an instance, the instance name is appended:

```text
codexcli-<workspace-slug>-<hash>-<instance>
```

Separate worktrees therefore have stable, distinct names and can run in
parallel. Named instances also have distinct container names. A wrapper refuses
to start when its exact container name already exists; inspect or remove that
container explicitly before retrying.

Named instances in the same checkout share the mounted workspace. Use them only
for read-only work or clearly disjoint files. For overlapping tracked-code
work, use separate Git worktrees instead. Container separation does not prevent
agents from editing the same files, index, or branch, so changes can be
overwritten or combined unpredictably.

Do not run two agents in the same checkout. Container separation does not
prevent both agents from editing the same files, index, or branch, so changes
can be overwritten or combined unpredictably. Use one Git worktree per agent.
All instances share `~/.local/share/codexcli/codex-home` by default. This is
useful for auth and config reuse, but sessions, history, and lock files may
also be shared.

See `docs/runtime-notes.md` for runtime caveats, including Git hook handling in
mounted repositories.
10 changes: 10 additions & 0 deletions docs/runtime-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,13 @@ modify `.git/config` in mounted repositories.
Treat `.git/hooks` and `.git/config` as agent-writable after a session in an
untrusted workspace. Review them before host-side commits, checkouts, or other
Git operations that may execute hooks or rely on repository-local config.

## Named instances

Named instances isolate container names, not the workspace. They do not isolate
the Git index, current branch, Git common directory, or Codex home. Use them for
read-only checks or clearly disjoint files; use separate Git worktrees for real
parallel implementation work.

`codexcli-login` parallelism is still limited by the fixed
`127.0.0.1:1455` login callback port.
103 changes: 97 additions & 6 deletions scripts/lib-codexcli-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,22 @@ codexcli_container_name() {
printf 'codexcli-%s-%s\n' "$workspace_basename" "$workspace_hash"
}

codexcli_validate_instance() {
case "$1" in
''|*[!abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-]*)
codexcli_die "invalid instance name: use only letters, numbers, dot, underscore, and dash"
;;
esac
}

codexcli_effective_container_name() {
if [ -n "$CODEXCLI_INSTANCE" ]; then
printf '%s-%s\n' "$CODEXCLI_BASE_CONTAINER_NAME" "$CODEXCLI_INSTANCE"
else
printf '%s\n' "$CODEXCLI_BASE_CONTAINER_NAME"
fi
}

codexcli_check_container_name() {
local exists_status

Expand Down Expand Up @@ -55,12 +71,70 @@ codexcli_require_git_identity() {
codexcli_run() {
local doctor=false
local git_common_mount workspace_mount
local cli_instance_set=false
local env_instance_set=false
local wrapper_ended=false
local -a podman_args

if [ "${1-}" = "--doctor" ]; then
doctor=true
shift
[ "$#" -eq 0 ] || codexcli_die "--doctor does not accept arguments"
if [ "${CODEXCLI_INSTANCE+x}" = x ]; then
env_instance_set=true
else
CODEXCLI_INSTANCE=''
fi

while [ "$#" -gt 0 ]; do
case "$1" in
--instance)
shift
[ "$#" -gt 0 ] || codexcli_die "--instance requires a value"
case "$1" in
--|--doctor|--instance|--instance=*)
codexcli_die "--instance requires a value"
;;
esac
CODEXCLI_INSTANCE="$1"
cli_instance_set=true
codexcli_validate_instance "$CODEXCLI_INSTANCE"
shift
;;
--instance=*)
CODEXCLI_INSTANCE="${1#--instance=}"
cli_instance_set=true
codexcli_validate_instance "$CODEXCLI_INSTANCE"
shift
;;
--doctor)
doctor=true
shift
;;
--)
shift
wrapper_ended=true
break
;;
*)
break
;;
esac
done

if [ "$cli_instance_set" = false ] && [ "$env_instance_set" = true ]; then
[ -n "$CODEXCLI_INSTANCE" ] || codexcli_die "CODEXCLI_INSTANCE must not be empty"
codexcli_validate_instance "$CODEXCLI_INSTANCE"
fi

if [ "$doctor" = true ] && [ "$#" -gt 0 ]; then
codexcli_die "--doctor does not accept Codex arguments"
fi

if [ "$wrapper_ended" = false ]; then
for arg in "$@"; do
case "$arg" in
--instance|--instance=*)
codexcli_die "--instance must appear before Codex arguments; use -- to pass it to Codex"
;;
esac
done
fi

command -v git >/dev/null 2>&1 || codexcli_die "git not found"
Expand All @@ -71,7 +145,9 @@ codexcli_run() {
|| codexcli_die "current directory is not inside a Git repository"
CODEXCLI_GIT_COMMON_DIR="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \
|| codexcli_die "could not resolve Git common directory"
CODEXCLI_CONTAINER_NAME="$(codexcli_container_name)"
CODEXCLI_BASE_CONTAINER_NAME="$(codexcli_container_name)"
CODEXCLI_CONTAINER_NAME="$(codexcli_effective_container_name)"
CODEXCLI_INSTANCE_DISPLAY="${CODEXCLI_INSTANCE:-<default>}"
CODEXCLI_HOME_HOST="${HOME}/.local/share/codexcli/codex-home"
codexcli_require_git_identity

Expand Down Expand Up @@ -110,7 +186,15 @@ codexcli_run() {
)

if [ "$doctor" = true ]; then
podman_args+=(--security-opt label=disable -e GIT_OPTIONAL_LOCKS=0)
podman_args+=(
--security-opt label=disable
-e GIT_OPTIONAL_LOCKS=0
-e "CODEXCLI_DOCTOR_WORKSPACE=$CODEXCLI_WORKSPACE"
-e "CODEXCLI_DOCTOR_BASE_CONTAINER_NAME=$CODEXCLI_BASE_CONTAINER_NAME"
-e "CODEXCLI_DOCTOR_INSTANCE=$CODEXCLI_INSTANCE_DISPLAY"
-e "CODEXCLI_DOCTOR_CONTAINER_NAME=$CODEXCLI_CONTAINER_NAME"
-e "CODEXCLI_DOCTOR_HOME_HOST=$CODEXCLI_HOME_HOST"
)
else
podman_args+=(-v "$CODEXCLI_HOME_HOST:/home/codex/.codex:Z")
fi
Expand All @@ -134,6 +218,13 @@ codexcli_run() {
git config --get "$2" || printf "<not configured>\n"
}

printf "workspace: %s\n" "$CODEXCLI_DOCTOR_WORKSPACE"
printf "base container name: %s\n" "$CODEXCLI_DOCTOR_BASE_CONTAINER_NAME"
printf "instance: %s\n" "$CODEXCLI_DOCTOR_INSTANCE"
printf "effective container name: %s\n" "$CODEXCLI_DOCTOR_CONTAINER_NAME"
printf "host Codex home (normal mode): %s\n" "$CODEXCLI_DOCTOR_HOME_HOST"
printf "container CODEX_HOME: %s\n" "$CODEX_HOME"
printf "doctor mode: temporary Codex home; workspace and Git mounts are read-only\n"
pwd
python3 --version
node --version
Expand Down
Loading