Skip to content
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ this repo. They are service-side state, not portable files.
- `AGENTS.md`: repo-local maintenance guidance for this portable config repo.
Use this for codex-portable process and review rules.
- `codex/keybindings.json`: portable keyboard bindings.
- `codex/hooks.json` and `codex/hooks/`: reviewed Codex hooks installed into
the live Codex home. Hooks require `/hooks` trust review after install.
- `codex/agents/`: reusable global custom agents installed into the live Codex
home. Project-specific custom agents belong in the target repo.
- `codex/skills/`: reusable global custom skills installed into the live Codex
Expand Down
44 changes: 44 additions & 0 deletions codex/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"hooks": {
"PreToolUse": [
{
"matcher": "^(Bash|apply_patch)$",
"hooks": [
{
"type": "command",
"command": "sh \"${CODEX_HOME:-$HOME/.codex}/hooks/portable_guard.sh\"",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$home = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE '.codex' }; $script = Join-Path $home 'hooks\\portable_guard.ps1'; if (Test-Path -LiteralPath $script) { & $script }\"",
"timeout": 30,
"statusMessage": "Checking portable workflow guard"
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "sh \"${CODEX_HOME:-$HOME/.codex}/hooks/portable_guard.sh\"",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$home = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE '.codex' }; $script = Join-Path $home 'hooks\\portable_guard.ps1'; if (Test-Path -LiteralPath $script) { & $script }\"",
"timeout": 30,
"statusMessage": "Checking understanding prompt"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "sh \"${CODEX_HOME:-$HOME/.codex}/hooks/portable_guard.sh\"",
"commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -Command \"$home = if ($env:CODEX_HOME) { $env:CODEX_HOME } else { Join-Path $env:USERPROFILE '.codex' }; $script = Join-Path $home 'hooks\\portable_guard.ps1'; if (Test-Path -LiteralPath $script) { & $script }\"",
"timeout": 30,
"statusMessage": "Checking git closeout"
}
]
}
]
}
}
27 changes: 27 additions & 0 deletions codex/hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Portable Codex Hooks

These hooks are global Codex hooks carried by `codex-portable`.

- Dirty worktree closeout: on `Stop`, asks Codex to continue once when the current repo or immediate child repos have dirty files or unpushed commits, so the final answer names the leftover state.
- Public artifact dash guard: on `PreToolUse`, blocks public commit, tag, and PR commands that contain Unicode dash characters, and blocks any patch that adds those characters.
- Understanding check focus: on `UserPromptSubmit`, asks Codex to answer only
the user's understanding check when the prompt directly asks "do you
understand what I mean", `dykwim`, or `ykwim`.

The hook commands resolve from the active Codex home. If `CODEX_HOME` is set,
they run from that home instead of assuming the default `~/.codex`.

The launchers fail open if the guard script or Python runner is missing. The
portable repo checks catch that state before the hook is treated as accepted.

Opt-outs:

- `CODEX_PORTABLE_DISABLE_DASH_GUARD=1` disables dash blocking.
- `CODEX_PORTABLE_DISABLE_GIT_CLOSEOUT=1` disables dirty or unpushed git
closeout.
- `CODEX_PORTABLE_DISABLE_CHILD_REPO_SCAN=1` keeps git closeout limited to the
current repository.
- `CODEX_PORTABLE_DISABLE_UNDERSTANDING_CHECK=1` disables understanding check
focus.

Codex requires hook trust review after these hooks are installed into a live Codex home. Open `/hooks` and trust the current definitions.
61 changes: 61 additions & 0 deletions codex/hooks/portable_guard.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
$ErrorActionPreference = "SilentlyContinue"

if ($env:CODEX_HOME) {
$codexHome = $env:CODEX_HOME
}
else {
$codexHome = Join-Path $env:USERPROFILE ".codex"
}

$guard = Join-Path $codexHome "hooks\portable_guard.py"
if (-not (Test-Path -LiteralPath $guard)) {
exit 0
}

$runners = @(
@("py", "-3"),
@("python3"),
@("python")
)

foreach ($runner in $runners) {
$exe = $runner[0]
if (-not (Get-Command $exe -ErrorAction SilentlyContinue)) {
continue
}

$runnerArgs = @()
if ($runner.Count -gt 1) {
$runnerArgs = @($runner[1..($runner.Count - 1)])
}

& $exe @runnerArgs "--version" *> $null
if ($LASTEXITCODE -eq 0) {
$pipelineInput = @($input)
if ($pipelineInput.Count -gt 0) {
$inputText = $pipelineInput -join [Environment]::NewLine
}
else {
$inputText = [Console]::In.ReadToEnd()
}
$oldPythonIoEncoding = [Environment]::GetEnvironmentVariable("PYTHONIOENCODING", "Process")
$oldOutputEncoding = $global:OutputEncoding
$oldConsoleOutputEncoding = [Console]::OutputEncoding
$utf8Encoding = New-Object System.Text.UTF8Encoding $false
[Environment]::SetEnvironmentVariable("PYTHONIOENCODING", "utf-8", "Process")
$global:OutputEncoding = $utf8Encoding
[Console]::OutputEncoding = $utf8Encoding
try {
$inputText | & $exe @runnerArgs $guard
$guardExitCode = $LASTEXITCODE
}
finally {
[Environment]::SetEnvironmentVariable("PYTHONIOENCODING", $oldPythonIoEncoding, "Process")
$global:OutputEncoding = $oldOutputEncoding
[Console]::OutputEncoding = $oldConsoleOutputEncoding
}
exit $guardExitCode
}
}

exit 0
Loading