- Path:
docs/setup/ubuntu/auth.md - Template Version:
20260508
This document describes how to configure credentials for GitHub CLI and Codex CLI when running Codex agents through GitHub Flows.
The credentials are used by Docker-isolated agent executions.
This document covers:
- GitHub personal access token storage;
- GitHub App authentication via
hostScriptwith short-lived installation tokens; - GitHub CLI authentication through
GH_TOKEN; - Codex authentication through persisted Codex auth state;
- host-side preparation of execution-scoped credentials or helper files;
- profile configuration for passing credentials into the agent container.
This document does not describe Docker image creation, application deployment, Apache configuration, or GitHub webhook setup.
The runtime user is referenced as user.
The application is deployed into:
/home/user/app/github-flows/
The runtime workspace is located under:
/home/user/app/github-flows/var/work/
The Codex agent image is:
github-flows-agent-codex:latest
The long-lived GitHub token file is:
/home/user/.secrets/gh-token
The Codex auth state directory is:
/home/user/.secrets/codex/
Inside the container, only selected run inputs should appear:
/run/secrets/gh-token execution-scoped GitHub token file or bind mount
/home/user/.codex Codex auth state
/workspace per-run workspace
Before the container starts, a selected runtime profile may also create
execution-scoped files on the host through hostScript. Those files should be
mounted only into the run that needs them.
The GitHub account used by the agent must have access to the target repository.
For a private organization repository, organization membership alone may not be enough. The account must have repository access through direct repository permissions, team membership, or another approved organization access policy.
For a Codex agent that reads issues, modifies files, pushes branches, and opens pull requests, the GitHub account typically needs write access to the target repository.
Create a fine-grained personal access token for the GitHub account used by the agent.
Recommended repository access:
Only selected repositories:
owner/repository
Recommended repository permissions:
Metadata: Read
Contents: Read and write
Issues: Read and write
Pull requests: Read and write
If the repository belongs to an organization, the token may require organization owner approval before it can access private organization resources.
For organization approval, use the organization settings:
Organization
-> Settings
-> Personal access tokens
-> Pending requests
-> select the token request
-> Approve
Run under the runtime user:
sudo -iu userCreate the secrets directory:
mkdir -p /home/user/.secrets
chmod 700 /home/user/.secretsCreate the token file:
nano /home/user/.secrets/gh-token
chmod 600 /home/user/.secrets/gh-tokenThe file must contain only the raw GitHub token value.
This host-local secret is a source input for host-side preparation. It should not be copied into the runtime workspace as a durable file.
Check that the runtime user can read the token:
test -r /home/user/.secrets/gh-token && echo gh-token-okCheck the GitHub identity exposed by the token:
docker run --rm \
--mount type=bind,src=/home/user/.secrets/gh-token,dst=/run/secrets/gh-token,readonly \
-e GH_TOKEN_FILE=/run/secrets/gh-token \
github-flows-agent-codex:latest \
bash -lc 'export GH_TOKEN="$(tr -d "\r\n" < "$GH_TOKEN_FILE")"; export GITHUB_TOKEN="$GH_TOKEN"; gh api user --jq .login'Check repository access:
docker run --rm \
--mount type=bind,src=/home/user/.secrets/gh-token,dst=/run/secrets/gh-token,readonly \
-e GH_TOKEN_FILE=/run/secrets/gh-token \
github-flows-agent-codex:latest \
bash -lc 'export GH_TOKEN="$(tr -d "\r\n" < "$GH_TOKEN_FILE")"; export GITHUB_TOKEN="$GH_TOKEN"; gh repo view owner/repository'The tr -d "\r\n" command removes line endings from the token file before
exporting the token.
Use this split when preparing credentials for a run:
hostScriptkeeps long-lived secrets on the host, derives any temporary execution-scoped files, and decides which files are mounted into the selected container;setupScriptconsumes only the already-mounted run inputs from inside the container.
Examples of execution-scoped artifacts:
- a short-lived token file derived from a host-local source secret;
- a temporary context snapshot prepared for one run;
- a run-local helper file needed only by the selected profile.
Remove such artifacts after the run. Do not promote them into long-lived host storage or public inspection paths.
Instead of a long-lived personal access token, you can use a GitHub App to
generate short-lived installation tokens for each agent run. The token is
produced by hostScript before the container starts and written into the
per-run workspace, making it available inside the container without any
extra Docker mount.
- Tokens expire after 1 hour (vs. static PAT that never expires).
- Each token is scoped to one installation — no user-bound PAT to rotate.
- The App private key is never mounted into the container.
- Works with parallel agent executions — each run gets its own isolated token file inside its own workspace.
- Go to GitHub Settings → Developer settings → GitHub Apps → New GitHub App.
- Give it a name, e.g.
github-flows-agent. - Disable webhook (the app is only used for token generation).
- Set Permissions:
Contents: Read and writeIssues: Read and writePull requests: Read and writeMetadata: Read
- Set Where can this GitHub App be installed? →
Any account. - Click Create GitHub App.
- On the app page, scroll to Private keys → Generate a private key.
A
.pemfile is downloaded. Keep it secure.
- Go to the GitHub App page → Install App → select the target account or organization.
- Choose Only selected repositories and pick the repository where agents will operate.
- Click Install.
- Note the Installation ID — it appears in the URL after installation:
https://github.com/settings/installations/<INSTALLATION_ID>.
sudo -iu user
mkdir -p /home/user/.secrets
chmod 700 /home/user/.secrets
# Upload the downloaded PEM file
mv ~/github-flows-agent.2025-01-01.private-key.pem /home/user/.secrets/github-app.pem
chmod 600 /home/user/.secrets/github-app.pemAdd hostScript and App-related env vars to the profile runtime section.
The hostScript calls bin/gh-app-token.sh (shipped with the application)
which generates the token. The GH_TOKEN_FILE is set to a relative path
so the token lands inside the per-run workspace.
{
"execution": {
"runtime": {
"hostScript": "/home/user/app/github-flows/bin/gh-app-token.sh",
"env": {
"LOG_LEVEL": "info",
"GH_TOKEN_FILE": ".gh-token",
"GH_APP_ID": "123456",
"GH_APP_INSTALLATION_ID": "789012",
"GH_APP_KEY_FILE": "/home/user/.secrets/github-app.pem"
},
"dockerArgs": [
"--mount",
"type=bind,src=/home/user/.secrets/codex,dst=/home/user/.codex"
]
}
}
}Note: the gh-token Docker mount is no longer needed — the token file
lives inside the workspace which is already mounted as /workspace.
The setupScript reads the token from the workspace-relative path:
export GH_TOKEN="$(tr -d '\r\n' < "$GH_TOKEN_FILE")"
export GITHUB_TOKEN="$GH_TOKEN"GitHub Webhook Event
→ runtime selects profile
→ workspacePath created at <workspaceRoot>/ws/<owner>/<repo>/<event>/<id>/
→ hostScript: bin/gh-app-token.sh
→ reads GH_APP_KEY_FILE, GH_APP_ID, GH_APP_INSTALLATION_ID
→ builds JWT (RS256, 10 min validity)
→ POST /app/installations/{id}/access_tokens
→ writes token to workspacePath/.gh-token
→ Docker: mount workspacePath → /workspace
→ setupScript: reads /workspace/.gh-token
→ codex runs with short-lived GH_TOKEN
After the run, the workspace (including the token file) is cleaned up by the runtime.
Codex CLI supports authentication through a ChatGPT account or through an OpenAI API key.
For subscription-based Codex use, authenticate Codex through ChatGPT sign-in and persist the Codex auth state outside the container image.
Create the Codex auth directory:
sudo -iu user
mkdir -p /home/user/.secrets/codex
chmod 700 /home/user/.secrets/codexRun Codex login interactively:
docker run --rm -it \
--name github-flows-codex-login \
--mount type=bind,src=/home/user/.secrets/codex,dst=/home/user/.codex \
--mount type=bind,src=/home/user/app/github-flows/var/work,dst=/workspace \
-w /workspace \
github-flows-agent-codex:latest \
codex loginComplete the browser or device authentication flow shown by Codex.
After login, check that Codex auth state was written:
find /home/user/.secrets/codex -maxdepth 3 -type f -lsRun Codex interactively with the persisted auth state:
docker run --rm -it \
--name github-flows-codex-interactive \
--mount type=bind,src=/home/user/.secrets/codex,dst=/home/user/.codex \
--mount type=bind,src=/home/user/app/github-flows/var/work,dst=/workspace \
-w /workspace \
github-flows-agent-codex:latest \
codexInside Codex, select the model:
/model
Check the active session settings:
/status
The selected model and Codex settings are stored in the mounted Codex auth/config directory.
A profile can mount both long-lived credential sources into the container:
{
"runtime": {
"env": {
"LOG_LEVEL": "info",
"GH_TOKEN_FILE": "/run/secrets/gh-token"
},
"dockerArgs": [
"--mount",
"type=bind,src=/home/user/.secrets/codex,dst=/home/user/.codex",
"--mount",
"type=bind,src=/home/user/.secrets/gh-token,dst=/run/secrets/gh-token,readonly"
]
}
}This does not automatically create GH_TOKEN.
The host-side preparation step is responsible for deciding what
GH_TOKEN_FILE points to for the selected run. The container command must then
read GH_TOKEN_FILE and export GH_TOKEN before starting Codex.
In the newer runtime model, prefer splitting host-side and container-side work:
hostScriptprepares execution-scoped artifacts on the host, such as a copied token file under a run-specific temporary directory;setupScriptperforms lightweight container checks after the mounts are in place;- the execution command exports
GH_TOKENand starts Codex.
This profile launches Codex through bash -lc, uses hostScript to prepare an
execution-scoped token file on the host, uses setupScript for an in-container
sanity check, then reads the mounted token and starts Codex. The bind-mount
src value remains a host path, even though the destination is inside the
container.
{
"trigger": {
"repository": "owner/repository",
"event": "issue_comment",
"action": "created"
},
"execution": {
"handler": {
"type": "agent",
"command": ["bash", "-lc"],
"args": [
"export GH_TOKEN=\"$(tr -d '\\r\\n' < \"$GH_TOKEN_FILE\")\"; export GITHUB_TOKEN=\"$GH_TOKEN\"; exec codex exec --dangerously-bypass-approvals-and-sandbox -C /workspace/repo"
],
"promptRef": "prompt.md",
"promptVariables": {
"REPOSITORY": "event.repository.full_name",
"ISSUE_NUMBER": "event.issue.number",
"ISSUE_TITLE": "event.issue.title",
"ISSUE_BODY": "event.issue.body",
"ISSUE_AUTHOR": "event.issue.user.login"
}
},
"runtime": {
"image": "github-flows-agent-codex:latest",
"hostScript": "set -euo pipefail; exec_root=\"/home/user/app/github-flows/var/work/tmp/gh-auth/${EVENT_ID}\"; rm -rf \"$exec_root\"; mkdir -p \"$exec_root\"; install -m 600 /home/user/.secrets/gh-token \"$exec_root/gh-token\"",
"setupScript": "test -d repo && test -r /run/secrets/gh-token",
"timeoutSec": 1800,
"env": {
"LOG_LEVEL": "info",
"GH_TOKEN_FILE": "/run/secrets/gh-token"
},
"dockerArgs": [
"--mount",
"type=bind,src=/home/user/.secrets/codex,dst=/home/user/.codex",
"--mount",
"type=bind,src=/home/user/app/github-flows/var/work/tmp/gh-auth/${EVENT_ID}/gh-token,dst=/run/secrets/gh-token,readonly"
]
}
}
}The exact placeholder syntax for values such as ${EVENT_ID} depends on the
runtime package version and its profile-templating model. The important
repository-level rule is the split of responsibilities:
hostScriptprepares a run-specific file on the host;setupScriptvalidates the mounted result in the container;- the long-lived source secret stays outside the workspace-visible runtime state.
Prepare a test workspace:
mkdir -p /home/user/app/github-flows/var/work/auth-testRun a container with both GitHub and Codex credentials mounted:
docker run --rm -it \
--name github-flows-auth-test \
--mount type=bind,src=/home/user/.secrets/codex,dst=/home/user/.codex \
--mount type=bind,src=/home/user/.secrets/gh-token,dst=/run/secrets/gh-token,readonly \
--mount type=bind,src=/home/user/app/github-flows/var/work/auth-test,dst=/workspace \
-e GH_TOKEN_FILE=/run/secrets/gh-token \
-w /workspace \
github-flows-agent-codex:latest \
bash -lc 'export GH_TOKEN="$(tr -d "\r\n" < "$GH_TOKEN_FILE")"; export GITHUB_TOKEN="$GH_TOKEN"; gh api user --jq .login && codex --help'Do not mount the runtime user home directory:
/home/user
Do not mount SSH configuration:
/home/user/.ssh/
Do not mount the full user configuration directory:
/home/user/.config/
Do not mount the application .env file into the agent workspace:
/home/user/app/github-flows/.env
Do not mount the Docker socket:
/var/run/docker.sock
Do not place credentials inside:
/home/user/app/github-flows/var/work/
The runtime workspace may be visible through logs or debugging tools. It must not contain long-lived credentials.
If host-side preparation creates execution-scoped files for a selected run, clean them up after the run completes and keep them out of long-lived indexed or public paths.
If hostScript creates a temporary credential file under a run-specific
workspace directory, remove it after the run and do not reuse it across
executions.
After this setup:
- the GitHub token is stored in
/home/user/.secrets/gh-token; - the GitHub token file is readable only by the runtime user;
- host-side preparation may derive temporary execution-scoped token files from that host-local source when a selected run needs them;
- the Codex auth state is stored in
/home/user/.secrets/codex/; - GitHub CLI receives
GH_TOKENandGITHUB_TOKENfrom the mounted token file; - Codex CLI uses the mounted Codex auth state from
/home/user/.codex; - a selected profile may create an execution-scoped token copy for one run without moving the long-lived source secret into the workspace;
- profile execution can authenticate to GitHub without mounting the host home directory;
- profile execution can authenticate Codex without storing Codex credentials in the image;
- credentials are mounted into the container only at runtime;
- execution-scoped artifacts should be removed after the run;
- the per-run workspace remains separated from long-lived credentials.