Skip to content

Clean up CLI launch logs in temp (SBS-888) - #313

Merged
btsouth merged 5 commits into
mainfrom
tsouth2/sbs-888-clean-up-cli-launch-logs
Aug 16, 2026
Merged

Clean up CLI launch logs in temp (SBS-888)#313
btsouth merged 5 commits into
mainfrom
tsouth2/sbs-888-clean-up-cli-launch-logs

Conversation

@btsouth

@btsouth btsouth commented Aug 15, 2026

Copy link
Copy Markdown
Owner

Summary

  • SBS-888: Every codexbar process appended a PID-scoped log under the system temp dir and never removed it.
  • statusline is invoked once per editor render (SBS-271), so an active session left hundreds to thousands of files.
  • PID reuse appended to an existing file, so a colliding PID grew without bound.
  • Windows %TEMP% is not reliably purged across reboots.

Fix

  1. statusline writes no launch log at all. It reads cached state and reports failures through its host, so there is no launch problem a temp file would explain. Checked off raw argv, before clap, since main writes before parsing.
  2. The log opens truncating (.write(true).truncate(true)), so a reused PID cannot append across unrelated runs.
  3. A run that exits SUCCESS removes its own log. Steady state is zero files.
  4. Leftovers are swept after 24 hours, bounded to 512 removals per run. The name check is a cheap string compare, so metadata is only paid for actual launch logs, not every file in temp.

A run that fails still keeps its log, which is the only case worth a post-mortem.

The non-obvious part

Cli::parse() calls std::process::exit itself for --help, --version, and usage errors. That skipped main's cleanup entirely and stranded a file every time. Caught it in end-to-end testing, not review. Fixed with try_parse() + removing the log before deferring to Error::exit(), which preserves clap's own output and exit codes.

Verification

End-to-end against a scratch %TEMP%, before and after:

Invocation Before After
statusline x3 3 files 0
--version 1 file 0
--help 1 file 0
bad subcommand 1 file 0
usage -p claude (exit 0) 1 file 0
account switch claude no-such-account (exit 1) 1 file 1 file (kept, by design)

Tests

7 tests in main.rs, 6 new:

Test Covers
only_statusline_skips_the_launch_log the skip is narrow
launch_log_names_are_recognized_without_matching_neighbours the sweep does not eat unrelated temp files
starting_a_launch_log_truncates_a_reused_path PID-reuse growth
sweep_removes_stale_launch_logs_but_spares_mine_and_unrelated_files sweep targeting
sweep_keeps_launch_logs_inside_the_age_bound age bound
sweep_tolerates_a_missing_directory unreadable temp dir

Test plan

  • cargo test --bin codexbar - 7 passed
  • End-to-end table above
  • cargo clippy --all-targets -- -D warnings clean
  • cargo fmt --check clean
  • cargo test --lib - 1012 passed, 1 failed

Pre-existing failure, not from this PR: cli::tty_runner::tests::test_run_sends_script_through_pty fails on this machine because where.exe cmd finds an extensionless npm shim before System32\cmd.exe. Confirmed it fails identically on a clean main checkout. That is GitHub issue #270.

Note

Move CLI launch logs to a private per-user cache directory

  • Logs now go to <cache>/Ceiling/launch-logs/ instead of the system temp folder, with the directory created at 0o700 on Unix and ownership/mode verified before use.
  • The statusline subcommand skips log creation entirely; successful runs and usage errors delete their log on exit; only genuine failures retain logs.
  • Stale logs older than 24 hours are swept each run (bounded by scan/removal limits); legacy temp-folder logs are swept on initial runs after upgrade and then retired via a marker file.
  • Log file creation uses O_CREAT|O_EXCL and O_NOFOLLOW on Unix to prevent symlink attacks, and appending refuses to write through non-regular files or symlinks.
  • Behavioral Change: logs no longer accumulate in the system temp folder, and successful runs leave no log file behind.

Macroscope summarized d217335.

Every `codexbar` process appended a PID-scoped log under the system temp
dir and never removed it. `statusline` is invoked once per editor render
(SBS-271), so an active session left hundreds to thousands of files. PID
reuse appended to an existing file, so a colliding PID grew without bound.
Windows `%TEMP%` is not reliably purged.

Four changes:
- `statusline` writes no launch log at all. It reads cached state and
  reports through its host, so there is no launch failure a temp file
  would explain. Checked off raw argv, before clap.
- The log opens truncating, so a reused PID cannot append across runs.
- A run that exits SUCCESS removes its own log. Steady state is no files.
- Leftovers from failed or killed runs are swept after 24 hours, bounded
  to 512 removals per run.

`Cli::parse()` exits the process itself for `--help`, `--version`, and
usage errors, which skipped the cleanup and stranded a file. Use
`try_parse()` and remove the log before deferring to `Error::exit()`,
which keeps clap's own output and exit codes.

A run that fails still keeps its log.
@coderabbitai

coderabbitai Bot commented Aug 15, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 29 minutes

Limit details: You’ve used all 1 included review currently available under your plan. You completed 92 included PR reviews in the past 7 days; at that activity level, included reviews refill at 1 review per hour.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cd6905ae-7fcc-4529-9c37-6a0966b42d5f

📥 Commits

Reviewing files that changed from the base of the PR and between caa7fc3 and d217335.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • CHANGELOG.md
  • rust/Cargo.toml
  • rust/src/main.rs

Comment @coderabbitai help to get the list of available commands.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 15, 2026

Copy link
Copy Markdown
Contributor

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
ceiling d217335 Commit Preview URL

Branch Preview URL
Aug 16 2026, 02:55 AM

@github-actions

github-actions Bot commented Aug 15, 2026

Copy link
Copy Markdown

Automated review

New in this pass: 6 issues.

  1. append_launch_log writes through a symlink start could not replace

    rust/src/main.rs:95 · disposition: block · confidence: high · severity: high · quick win

    start_launch_log swallows a failed remove_file/create_new and main still passes that path into run(). append_launch_log then opens the existing path for append, which follows a symlink. On a shared temp an attacker who owns the well-known launch-log directory plants codexbar_launch_.log as a symlink to a victim-writable file; the victim cannot unlink it, start fails, and append writes the launch header into the target. The new test only covers a missing path, not an existing planted link. This is not the old truncating-open hole: create_new no longer truncates, but append still writes when replace fails.

    Prompt for AI agents

    In rust/src/main.rs around line 95: Make start_launch_log return whether create_new succeeded, and skip append and success-cleanup when it did not. Add a test that plants a symlink, makes unlink fail (parent not writable), calls start then append, and asserts the symlink target is unchanged. Verify against the current code first; if no longer valid, skip with a brief reason. Keep the change minimal.

  2. launch-log directory ownership not verified allows attacker-controlled directory

    rust/src/main.rs:22 · disposition: block · confidence: medium · severity: high

    launch_log_dir_in calls create_dir_all(&dir) then only checks is_dir via symlink_metadata. If an attacker pre-creates $TMP/codexbar-launch-logs as a directory they own (755) before the victim runs, create_dir_all succeeds and the check passes, so the victim writes into attacker-owned directory. The attacker can then plant symlinks, flood the directory, or observe logs. On Windows %TEMP% is per-user so not exploitable, but on Linux/WSL /tmp is shared and this is the realistic path.

    Prompt for AI agents

    In rust/src/main.rs around line 22: Verify the directory is owned by the current uid and not world-writable, or create it with 0o700 and verify ownership after create_dir_all; refuse and return None if owned by another user. Add test for pre-existing directory owned by other user. Verify against the current code first; if no longer valid, skip with a brief reason. Keep the change minimal.

  3. launch_log_dir_in accepts any real directory, including one another user owns

    rust/src/main.rs:33 · disposition: block · confidence: high · severity: medium · quick win

    After create_dir_all, the only check is symlink_metadata().is_dir(). A pre-created directory at the well-known name /tmp/codexbar-launch-logs or %TEMP%/codexbar-launch-logs is accepted with no owner or mode check. That lets another local user squat the directory and plant per-PID symlinks (the append write-through), read any logs this process does create, or, after a root/sudo first run leaves a 0755 dir they own, silently deny every later unprivileged run a launch log. The comment treats shared /tmp as in scope but only refuses a symlink, not a foreign directory.

    Prompt for AI agents

    In rust/src/main.rs around line 33: After creating the path, refuse it unless it is a directory owned by the current user, and chmod 0700 (Unix) or an equivalent current-user ACL (Windows). Add a test that a pre-existing directory we do not own, or a world-writable one, makes launch_log_dir_in return None. Verify against the current code first; if no longer valid, skip with a brief reason. Keep the change minimal.

  4. Usage errors that parse as Ok keep a launch log

    rust/src/main.rs:160 · disposition: fix-if-quick · confidence: high · severity: medium · quick win

    The new try_parse arm deletes the log for clap --help/--version/usage errors, but a no-subcommand invocation parses as Ok { command: None }. missing_subcommand then returns 64 and main keeps the file. That is the path for bare codexbar and for the documented form codexbar --provider all --brief. Each such run leaves codexbar_launch_.log until the 24-hour sweep, which is the leftover-file problem this change is trying to stop.

    Prompt for AI agents

    In rust/src/main.rs around line 160: Delete the launch log on USAGE_ERROR the same way the clap error arm does, or treat command == None as a clap-style usage error before returning. Add a unit test that a None-command / USAGE_ERROR path does not leave the file. Verify against the current code first; if no longer valid, skip with a brief reason. Keep the change minimal.

  5. skips_launch_log matches the token statusline in any argument position

    rust/src/main.rs:60 · disposition: fix-if-quick · confidence: high · severity: medium · quick win

    The comment says a bare scan cannot misfire because no global flag takes statusline as a value. Top-level and subcommand flags do take free strings: --provider/-p, --focus, --ssh-host, --label. codexbar usage --provider statusline therefore skips the launch log and then fails as an unknown provider, so the failure the log exists to record is the one that is not written.

    Prompt for AI agents

    In rust/src/main.rs around line 60: Treat only the first positional token (after skipping flags and the values of known value-taking flags) as the subcommand, and skip only when that token is statusline. Extend statusline_skips_the_launch_log_behind_global_flags so usage --provider statusline does not skip. Verify against the current code first; if no longer valid, skip with a brief reason. Keep the change minimal.

Also noted:

  • Changelog says leftovers from older runs are clearedCHANGELOG.md:13 · disposition: fix-if-quick · confidence: high · severity: low · quick win

Resolved since the previous pass: 6.

For coding agents: fix BLOCK and FIX IF QUICK findings now; everything else is tracked or informational; never exceed one CodeRev fix round per PR.

Advisory. Findings generated by grok-subscription and muse-spark-1.2-contributor, each filtered through a 3-vote refutation panel with the changed code in evidence.

Comment thread rust/src/main.rs Outdated
Comment thread rust/src/main.rs Outdated
Comment thread rust/src/main.rs
Comment thread rust/src/main.rs
Three findings from the advisory review, all real.

1. The truncating open followed symlinks. PIDs are predictable, so on a
   shared /tmp someone can pre-create the next run's log name as a link to
   a file they want destroyed; the truncating open would empty the target.
   The previous append-only code was not destructive, so this was a
   regression introduced by this branch.

   Unlink any existing entry first (unlinking a symlink never touches the
   target), then open with `create_new`, which is O_CREAT|O_EXCL and
   refuses to follow a link planted in the race window. Worst case now is
   losing the log, not the victim file. `append_launch_log` no longer
   creates, so it cannot resurrect a path that start refused.

2. `skips_launch_log` only checked argv[1], so `codexbar --verbose
   statusline` still wrote a log on every editor render. Scan all args
   instead. No global flag takes `statusline` as a value; `--log-level` is
   restricted to log levels.

3. The sweep bound capped removals, not the walk, so every non-statusline
   run still read_dir'd the whole temp root. Launch logs now live in
   `<temp>/codexbar-launch-logs/` and the sweep is scoped to it. The
   directory is refused if it is not a real directory, so the name cannot
   be used to redirect writes or the sweep.

New tests: planted-symlink target survives, symlinked log directory is
refused, append does not create, and statusline is detected behind each
global flag. The symlink test was confirmed to fail against the
truncating open.
Comment thread rust/src/main.rs
Comment thread rust/src/main.rs Outdated
Comment thread rust/src/main.rs Outdated
Comment thread CHANGELOG.md Outdated
Comment thread rust/src/main.rs Outdated
Comment thread rust/src/main.rs
The last round guarded the shared temp dir; this removes the exposure
instead. Review found the guards were not enough:

- `start_launch_log` swallowed a failed `create_new`, and `append_launch_log`
  then opened the same path for append, which follows a symlink. An
  attacker owning the directory could plant a link the victim cannot
  unlink, so the launch header was written into their chosen target. Not
  the old truncating hole; a second one behind it.
- `launch_log_dir_in` only refused a symlink, not a directory another user
  had already created at the well-known name.

Both come from putting a predictably named file in a world-writable
directory. Every fix for that is a patch on the location. Write to
`dirs::cache_dir()/Ceiling/launch-logs/` instead, which is inside the
user's home, so squatting and link planting do not apply, and the sweep
never walks a directory holding unrelated files. The symlink refusals
stay as defence in depth, and append now requires a regular file.

Also from review:
- Logs older versions wrote to the temp root were orphaned by the move, so
  the accumulated files this issue is about would never be cleaned. Sweep
  that legacy location too, age-bounded, never following a link.
- `skips_launch_log` matched the token anywhere, so
  `codexbar --provider statusline` skipped the log for a run that fails.
  Resolve the actual subcommand, consuming values of the flags that take
  one. Unknown flags are assumed to take none, which errs toward writing.
- `missing_subcommand` returns USAGE_ERROR through the `Ok` path, so bare
  `codexbar` kept a file every run. Keep logs only for real failures.

Verified end to end: statusline, --verbose statusline, --help, --version,
bad subcommand, bare codexbar, and a successful usage all leave zero
files; a genuine failure keeps one; the temp root stays clean.
@btsouth btsouth added coderev Approve CodeRev review for a first-time contributor PR and removed coderev Approve CodeRev review for a first-time contributor PR labels Aug 16, 2026
tsouth89 and others added 2 commits August 15, 2026 22:44
Four security findings on the launch-log path, plus the argv and sweep
follow-ups from the same review.

The log directory is now created 0700 and refused unless the current uid
owns it and no other account can reach it. A directory another user
pre-creates at the well-known name could hold planted per-PID symlinks,
read what we write, or - after a sudo first run - lock every later
unprivileged run out. The rule itself lives in `classify_log_dir`, split
from the filesystem so it is covered on Windows CI, where the unix branch
never runs.

The log file opens with `O_NOFOLLOW` alongside `O_CREAT | O_EXCL`, and
`start_launch_log` now reports whether the file is ours. `main` drops the
path when it is not, so a link that could not be unlinked - the victim
cannot write the squatted directory - no longer gets the launch header
appended into its target.

`skips_launch_log` resolves the subcommand with clap instead of reading
argv by hand. `argv[1]` missed `--verbose statusline` and put the
per-render path back on the write-and-sweep route; a bare token scan
would have misread `usage --provider statusline` and dropped the log for
the run that needs one.

The sweep caps entries examined, not only removals. The pass over the old
temp-root location retires itself with a marker once nothing is left
there, so steady-state runs never read_dir a `%TEMP%` holding tens of
thousands of files. Changelog says that rather than implying a permanent
temp cleanup.
@btsouth
btsouth merged commit a13248c into main Aug 16, 2026
11 of 12 checks passed
@btsouth
btsouth deleted the tsouth2/sbs-888-clean-up-cli-launch-logs branch August 16, 2026 02:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant