Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eef22c3
fix(workbench): share scan history across linked worktrees
mldangelo-oai Aug 15, 2026
20b170e
fix(workbench): verify repository generation across platforms
mldangelo-oai Aug 15, 2026
4f1eb91
fix(workbench): harden and simplify worktree history
mldangelo-oai Aug 15, 2026
e895dde
fix(workbench): preserve recreated repository subdirectories
mldangelo-oai Aug 15, 2026
6a2d4e3
fix(workbench): reconcile linked-worktree history with main
mldangelo-oai Aug 16, 2026
6283169
fix(workbench): refresh the bundled history plugin
mldangelo-oai Aug 16, 2026
088033d
fix(workbench): verify saved history candidates
mldangelo-oai Aug 16, 2026
b4b5c3a
fix(workbench): unify verified repository history
mldangelo-oai Aug 16, 2026
284acf7
fix(workbench): preserve scan history ownership and replay
mldangelo-oai Aug 16, 2026
d7b4b64
fix(workbench): quarantine unproved prerelease identities
mldangelo-oai Aug 16, 2026
c93dd73
fix(workbench): order automatic history comparisons
mldangelo-oai Aug 16, 2026
042fb82
fix(workbench): persist completion visibility order
mldangelo-oai Aug 16, 2026
337abb6
fix(workbench): preserve findings and Linux identities
mldangelo-oai Aug 16, 2026
7a9e4b6
fix(workbench): preserve unbound legacy history
mldangelo-oai Aug 16, 2026
e4694eb
Bind repository history to each scan generation
mldangelo-oai Aug 16, 2026
6f2f60b
Preserve archived scans across registration failures
mldangelo-oai Aug 16, 2026
4bbb75e
Bound repository identity maintenance to selected targets
mldangelo-oai Aug 16, 2026
10a6dd7
Repair legacy identity upgrades and precompute repository counts
mldangelo-oai Aug 16, 2026
f6651b2
Merge main into linked-worktree history repair
mldangelo-oai Aug 17, 2026
c829c5f
test(workbench): isolate directory metadata on Python 3.14
mldangelo-oai Aug 17, 2026
325b58d
Merge main session setup into linked-worktree history repair
mldangelo-oai Aug 17, 2026
27a1ad6
fix(sdk): exclude internal helpers from public declarations
mldangelo-oai Aug 17, 2026
c7f733d
Merge main and advance linked-history plugin identity
mldangelo-oai Aug 18, 2026
2d9965e
test(workbench): send identity fixture through stdin
mldangelo-oai Aug 18, 2026
a2f5676
fix(workbench): verify ownership before rejoining scans
mldangelo-oai Aug 18, 2026
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
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ container credentials, and the default AWS credential chain.

Local sign-in honors Codex's configured credential backend, including a system
keyring required by a managed device. Codex Security keeps login and scan
credentials in the same private, persistent state directory.
sign-in scoped to a dedicated Codex home in the same private, persistent state
directory.

If both a ChatGPT sign-in and an API key are available, interactive scans ask
which credential to use. CI and other noninteractive scans keep the existing
Expand All @@ -67,12 +68,26 @@ keys:
unset OPENAI_API_KEY CODEX_API_KEY
```

Scan history is stored in the Codex Security workbench state directory. If that
directory cannot be written, set `CODEX_SECURITY_STATE_DIR` to a writable
directory outside the repository.
Scan history and saved findings are stored in one Codex Security state database.
Linked Git worktrees are discovered automatically and grouped when they use the
same state directory. Leave `CODEX_SECURITY_STATE_DIR` unset, or select one
stable, writable directory outside the repository. Changing or unsetting it
selects separate history and an isolated Codex credential home and sign-in
scope; restore the previous value to reopen its existing scans and sign-in.

```bash
# Run from another linked Git worktree:
npx @openai/codex-security scans list
npx @openai/codex-security findings list

# Reopen an existing, separately selected state directory:
export CODEX_SECURITY_STATE_DIR=/path/to/existing/codex-security-state
npx @openai/codex-security scans list
```

`findings list [repository]` shows open findings across a repository's scans
and identifies findings not confirmed in its latest scan.
and identifies findings not confirmed in its latest completed scan across
linked worktrees.

Use `patch --linear-issue SEC-123` to import and fix a Linear issue, or
`patch --linear-project "Security backlog" --linear-filter '{"labels":{"name":{"eq":"security"}}}'`
Expand Down
61 changes: 49 additions & 12 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@ The CLI and SDK recognize the following user-configurable environment:
| `CODEX_SECURITY_LINEAR_API_KEY` | Patch Linear issues or publish directly with a personal API key. |
| `CODEX_SECURITY_LOG_LEVEL` | CLI-only; set to `debug` for verbose diagnostics. |
| `LOG_LEVEL` | CLI-only fallback when `CODEX_SECURITY_LOG_LEVEL` is unset. |
| `CODEX_SECURITY_STATE_DIR` | Override the private scan-history, workbench, and default artifact directory. |
| `CODEX_SECURITY_STATE_DIR` | Select the history database, artifact directory, and dedicated Codex credential home. |
| `CODEX_HOME` | Set the ambient Codex home for file-backed sign-in and default state; defaults to `~/.codex`. |
| `CODEX_CLI_PATH` | Use another Codex executable for authentication, plugin setup, scans, and nested workers. |
| `PYTHON` | Select a Python interpreter when `--python` or SDK `pythonPath` is not set. |
Expand Down Expand Up @@ -670,11 +670,27 @@ const directPublication = await publishScan("/path/to/completed-scan", {

### Scan history and reruns

`scans` or `scans list` lists scans for the current repository. Pass a repository
path to inspect another checkout, or `--scan-root DIR` to list scans whose
artifacts are under a particular root. `scans show` opens the latest completed
scan for the current repository. Pass `SCAN_ID` to inspect another scan. Scan
details include the configuration, results, coverage, and artifact locations. Add
`scans` or `scans list` lists scans for the current repository. Linked Git
worktrees are discovered automatically when their scans record the same Git
generation in the selected state database. The generation identifies the shared
Git and primary object-store directory instances; it is not a repository UUID.
History written without that scan-level evidence stays attached to its original
target and scan IDs. Older clients' history is not retroactively shared across
worktrees. Ordinary database opens do not retry unrelated unscanned targets;
eligible targets are bound when explicitly registered. On older Git, primary
`.git` files also need a repository-side `core.worktree` record identifying the
checkout. Layouts without that reverse record retain target-local history.
Explicit comparisons between independently verified copies of the same
repository remain available without joining their finding histories. Findings
indicate whether they were confirmed in the repository's latest completed scan
across those linked worktrees. Finding confirmation and
automatic matching use the order in which completed scans become visible in the
workbench; sealed report timestamps are unchanged. Pass a repository path to
inspect another checkout. `--scan-root DIR` only filters scans already recorded
in that database by their artifact directory; it never imports scan results from
another state directory. `scans show` opens the latest completed scan for the
current repository. Pass `SCAN_ID` to inspect another scan. Scan details include
the configuration, results, coverage, and artifact locations. Add
`--show-linked-findings` to include finding links from previous scans.

`scans logs` shows session events from the latest scan, including an active scan.
Expand All @@ -688,9 +704,27 @@ least eight characters.
Scan history uses `$CODEX_SECURITY_STATE_DIR/workbench.sqlite3` when
`CODEX_SECURITY_STATE_DIR` is set. Otherwise, it uses
`$CODEX_HOME/state/plugins/codex-security/workbench.sqlite3`; `CODEX_HOME`
defaults to `~/.codex`. Scan credentials are never stored in the scan
configuration. Recorded failure summaries and bulk-scan receipts omit messages
that contain recognizable credentials.
defaults to `~/.codex`. Saved findings use the same selected database.
Changing or unsetting `CODEX_SECURITY_STATE_DIR` selects separate scan history
and an isolated Codex credential home and sign-in scope; scans from the
previous state remain hidden until you select that state again. Keep the
setting stable across linked worktrees to share scans and findings
automatically.

```bash
# Inspect shared history from another linked Git worktree:
npx @openai/codex-security scans list
npx @openai/codex-security findings list

# Reopen an existing state directory and its sign-in:
export CODEX_SECURITY_STATE_DIR=/path/to/existing/codex-security-state
npx @openai/codex-security scans list
npx @openai/codex-security findings list
```

Scan credentials are never stored in the scan configuration. Recorded failure
summaries and bulk-scan receipts omit messages that contain recognizable
credentials.

The scan sandbox permits writes to the selected state directory so SQLite can
maintain its database and journal files. If the host itself cannot write to the
Expand All @@ -709,9 +743,12 @@ the same reason still applies.
Pass `SCAN_ID` to rerun another scan.

`scans match BEFORE_SCAN_ID AFTER_SCAN_ID` links findings with the same root
cause; `scans match --all` matches all completed scans of the current repository,
including other worktrees and clones. Saved matches appear in `scans show` and
are reused unless `--force` is passed. Scans without sealed artifacts are skipped.
cause. `scans match --all` matches eligible completed scans in the current
repository generation, including linked worktrees; legacy history without a
saved generation stays target-local. Explicit comparisons between separately
verified clones remain available without sharing their history. Saved matches
appear in `scans show` and are reused unless `--force` is passed. Scans without
sealed artifacts are skipped.

`scans compare` compares the two latest completed scans. Pass one scan ID to
compare it with the latest completed scan, or two IDs to select both scans. It
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/_bundled_plugin/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-security",
"version": "0.1.21",
"version": "0.1.31",
"description": "Codex Security workflows for security scans, analysis, and investigation.",
"author": {
"name": "OpenAI"
Expand Down
22 changes: 16 additions & 6 deletions sdk/typescript/_bundled_plugin/scripts/deep_scan_workbench.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
git_revision,
worktree_content_digest,
)
from workbench_target_state import RegisteredRepositoryTarget, require_scan_checkout_owner
from workbench_validation import optional_text, require_uuid, user_text

DEEP_SCAN_WORKER_KINDS = ("setup", "discovery", "dedup")
Expand Down Expand Up @@ -135,7 +136,7 @@ class DeepScanDependencies:
require_remediation_target: Callable[[str], Path]
require_scannable_target: Callable[[Path], None]
require_scope: Callable[[str, str, Path], str]
ensure_security_target: Callable[[sqlite3.Connection, str], str]
register_security_target: Callable[[sqlite3.Connection, str], RegisteredRepositoryTarget]
require_canonical_scan_directory: Callable[[Path], Path]
safe_segment: Callable[[str], str]
compact_timestamp: Callable[[], str]
Expand Down Expand Up @@ -194,8 +195,10 @@ def require_scope(scope: str, mode: str, target: Path) -> str:
return dependencies().require_scope(scope, mode, target)


def ensure_security_target(connection: sqlite3.Connection, target_path: str) -> str:
return dependencies().ensure_security_target(connection, target_path)
def register_security_target(
connection: sqlite3.Connection, target_path: str
) -> RegisteredRepositoryTarget:
return dependencies().register_security_target(connection, target_path)


def require_canonical_scan_directory(scan_dir: Path) -> Path:
Expand Down Expand Up @@ -617,6 +620,7 @@ def begin_deep_scan_for_scan(
) -> dict[str, Any]:
scan_id = require_uuid(scan_id, "scan-id")
candidate = require_scan(connection, scan_id)
require_scan_checkout_owner(connection, candidate)
workspace = require_workspace(connection, candidate["workspace_id"])
if (
candidate["mode"] == "deep"
Expand Down Expand Up @@ -678,6 +682,7 @@ def begin_deep_scan_for_scan(
connection.execute("BEGIN IMMEDIATE")
try:
scan, _ = require_owned_scan(connection, scan_id, thread_id)
require_scan_checkout_owner(connection, scan)
require_current_continuation(
scan,
args.claim_token,
Expand Down Expand Up @@ -717,6 +722,7 @@ def begin_deep_scan_for_target(
try:
existing = existing_deep_scan_for_target(connection, thread_id, target_path, scope)
if existing is not None:
require_scan_checkout_owner(connection, existing)
existing_run = connection.execute(
"SELECT 1 FROM deep_scan_runs WHERE scan_id = ?", (existing["id"],)
).fetchone()
Expand Down Expand Up @@ -752,6 +758,7 @@ def begin_deep_scan_for_target(
target_inode,
)
if terminal is not None:
require_scan_checkout_owner(connection, terminal)
connection.commit()
return deep_scan_result(
connection,
Expand All @@ -775,7 +782,8 @@ def begin_deep_scan_for_target(
workspace_id = str(uuid.uuid4())
scan_id = str(uuid.uuid4())
timestamp = now()
target_id = ensure_security_target(connection, target_path)
registration = register_security_target(connection, target_path)
target_id = registration.target_id
scan_dir = Path(
tempfile.mkdtemp(
prefix=f"{safe_segment(revision)}_{compact_timestamp()}_",
Expand Down Expand Up @@ -804,17 +812,19 @@ def begin_deep_scan_for_target(
connection.execute(
"""
INSERT INTO scans (
id, workspace_id, target_id, target_path, target_revision, target_snapshot_digest,
id, workspace_id, target_id, repository_generation, target_path,
target_revision, target_snapshot_digest,
target_device, target_inode, scope, mode, user_context,
deep_scan_owner_thread_id, scan_dir, model, reasoning_effort, status, phase,
handoff_status, started_at, created_at, updated_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'deep', ?, ?, ?, ?, ?,
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'deep', ?, ?, ?, ?, ?,
'running', 'preflight', 'delivered', ?, ?, ?)
""",
(
scan_id,
workspace_id,
target_id,
registration.repository_generation,
target_path,
revision,
target_snapshot_digest,
Expand Down
12 changes: 11 additions & 1 deletion sdk/typescript/_bundled_plugin/scripts/workbench_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ def parse_args(description: str) -> argparse.Namespace:

list_unmatched_scan_pairs = subparsers.add_parser("list-unmatched-scan-pairs")
list_unmatched_scan_pairs.add_argument("--repository", required=True)
list_unmatched_scan_pairs.add_argument("--after-scan-id")
list_unmatched_scan_pairs.add_argument("--force", action="store_true")

register_cli_scan = subparsers.add_parser("register-cli-scan")
Expand All @@ -147,6 +148,13 @@ def parse_args(description: str) -> argparse.Namespace:
register_cli_scan.add_argument("--archive-existing", action="store_true")
register_cli_scan.add_argument("--archived-scan-dir")

restore_cli_scan_archive = subparsers.add_parser("restore-cli-scan-archive")
restore_cli_scan_archive.add_argument("--scan-dir", required=True)
restore_cli_scan_archive.add_argument("--archived-scan-dir", required=True)
previous_scan = restore_cli_scan_archive.add_mutually_exclusive_group(required=True)
previous_scan.add_argument("--previous-scan-id")
previous_scan.add_argument("--previous-scan-absent", action="store_true")

set_scan_thread = subparsers.add_parser("set-scan-thread")
set_scan_thread.add_argument("--scan-id", required=True)
set_scan_thread.add_argument("--thread-id", required=True)
Expand All @@ -169,7 +177,9 @@ def parse_args(description: str) -> argparse.Namespace:
list_global_findings.add_argument("--query")
list_global_findings.add_argument("--severity", choices=FINDING_SEVERITIES)
list_global_findings.add_argument("--status", choices=FINDING_STATUSES)
list_global_findings.add_argument("--target-id")
finding_repository = list_global_findings.add_mutually_exclusive_group()
finding_repository.add_argument("--target-id")
finding_repository.add_argument("--repository")
list_global_findings.add_argument("--offset", type=non_negative_int, default=0)
list_global_findings.add_argument("--limit", type=positive_int, default=FINDINGS_PAGE_MAX)
list_repositories = subparsers.add_parser("list-repositories")
Expand Down
Loading
Loading