Summary
write_history_record() and append_history_line() bypass the directory- and symlink-hardening that every other private-file write path in this package (_private_files.write_private_json) already applies, so the history directory ends up world-readable and the append-only write is not protected against a planted symlink.
Details
lib/python/base_cli/history.py:167 (write_history_record) creates its target directory with a plain path.parent.mkdir(parents=True, exist_ok=True). Unlike every runtime directory created via _runtime.create_runtime_directory/_private_files.restrict_directory (chmod 0700), this call never restricts permissions — it inherits the process umask. Reproduced directly: with a default umask of 0022, calling write_history_record() on a fresh path leaves the whole directory chain (e.g. cache_root/<app>/history/) at 0755, even though the file written inside it (runs.jsonl) is correctly chmod'd 0600 immediately after.
lib/python/base_cli/history.py:207-235 (append_history_line) opens the destination with os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND | binary_flag, 0o600) — no O_NOFOLLOW, no dir-fd anchoring. _private_files.write_private_json (used for every other private runtime file in the package) goes to considerable lengths to avoid exactly this class of issue: it opens the parent directory component-by-component with O_NOFOLLOW/dir_fd (_open_parent_directory), and explicitly refuses to replace a destination that is already a symlink. append_history_line has none of that, so if the target directory is left world-writable (or was created by an earlier, unpatched version of this same function per the point above) a local attacker could plant a symlink at the history path and have this writer follow it.
Impact
The persisted command-history file (runs.jsonl) is one of the more sensitive artifacts this package writes — it aggregates redacted argv and metadata across every invocation. Leaving its containing directory world-listable exposes run IDs, timestamps, and directory structure to other local users even though the file content itself is protected, and the missing symlink guard is a real (if narrower) gap against the package's own established hardening standard elsewhere in the same module (_private_files.py).
Suggested fix
Route write_history_record's directory creation through _private_files.restrict_directory (or _runtime.create_runtime_directory) instead of a raw mkdir, and add os.O_NOFOLLOW (where available) to append_history_line's open flags, consistent with the rest of the package's file-write hardening.
Summary
write_history_record()andappend_history_line()bypass the directory- and symlink-hardening that every other private-file write path in this package (_private_files.write_private_json) already applies, so the history directory ends up world-readable and the append-only write is not protected against a planted symlink.Details
lib/python/base_cli/history.py:167(write_history_record) creates its target directory with a plainpath.parent.mkdir(parents=True, exist_ok=True). Unlike every runtime directory created via_runtime.create_runtime_directory/_private_files.restrict_directory(chmod0700), this call never restricts permissions — it inherits the process umask. Reproduced directly: with a default umask of0022, callingwrite_history_record()on a fresh path leaves the whole directory chain (e.g.cache_root/<app>/history/) at0755, even though the file written inside it (runs.jsonl) is correctly chmod'd0600immediately after.lib/python/base_cli/history.py:207-235(append_history_line) opens the destination withos.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND | binary_flag, 0o600)— noO_NOFOLLOW, no dir-fd anchoring._private_files.write_private_json(used for every other private runtime file in the package) goes to considerable lengths to avoid exactly this class of issue: it opens the parent directory component-by-component withO_NOFOLLOW/dir_fd(_open_parent_directory), and explicitly refuses to replace a destination that is already a symlink.append_history_linehas none of that, so if the target directory is left world-writable (or was created by an earlier, unpatched version of this same function per the point above) a local attacker could plant a symlink at the history path and have this writer follow it.Impact
The persisted command-history file (
runs.jsonl) is one of the more sensitive artifacts this package writes — it aggregates redacted argv and metadata across every invocation. Leaving its containing directory world-listable exposes run IDs, timestamps, and directory structure to other local users even though the file content itself is protected, and the missing symlink guard is a real (if narrower) gap against the package's own established hardening standard elsewhere in the same module (_private_files.py).Suggested fix
Route
write_history_record's directory creation through_private_files.restrict_directory(or_runtime.create_runtime_directory) instead of a rawmkdir, and addos.O_NOFOLLOW(where available) toappend_history_line's open flags, consistent with the rest of the package's file-write hardening.