Add --save-dir finalization: auto-name, note, and sidecar#200
Conversation
…uto-name Fold the post-capture index step into `assembly stream --save-dir` so a wrapper script no longer needs an index loop: - `--llm "…"` alongside `--save-dir` writes the final prompt-chain answer as a `.md` note next to the auto-named transcript (summarize-on-capture). - a `.aai.json` sidecar (title, date, duration, speakers, turns, file names) lands beside every recording so a list/browse UI needs no transcript parsing, and `--no-save-audio` keeps the transcript without the WAV. - `--auto-name` derives the filename slug from the transcript via the LLM and renames the files once the stream ends (mutually exclusive with `--name`). The --save-dir lifecycle lives in the new streaming/savedir.py (pure file I/O, unit-tested without a gateway); the batch driver moves to streaming/batch.py to keep session.py under the line limit. Docs updated in REFERENCE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KNx966tACLPYX4B5jkcfqp
| def _write(path: Path, text: str) -> None: | ||
| """Write ``text`` to ``path`` (the note or sidecar), as a clean CLIError on failure.""" | ||
| try: | ||
| path.write_text(text, encoding="utf-8") |
There was a problem hiding this comment.
Potential file inclusion attack via reading file - medium severity
If an attacker can control the input leading into the open function, they might be able to read sensitive files and launch further attacks with that information.
Show fix
Remediation: Ignore this issue only after you've verified or sanitized the input going into this function.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
There was a problem hiding this comment.
@AikidoSec ignore: False positive for this local CLI. savedir.py only ever writes (write_text/rename) to paths it assembles itself — never reads a user-supplied path. The destination is built from the user's own --save-dir/--name arguments plus, under --auto-name, an LLM-derived title that is run through naming.slugify (lowercased, every non-[a-z0-9] run collapsed to -, length-capped), so /, . and .. cannot survive into the filename — no path traversal or sensitive-file read is reachable. This matches the existing # nosemgrep precedent for the same class in aai_cli/app/transcribe/batch.py.
Generated by Claude Code
There was a problem hiding this comment.
✅ Based on your feedback, we ignored this issue because of the following reason:
False positive for this local CLI.
savedir.pyonly ever writes (write_text/rename) to paths it assembles itself — never reads a user-supplied path. The destination is built from the user's own--save-dir/--namearguments plus, under--auto-name, an LLM-derived title that is run throughnaming.slugify(lowercased, every non-[a-z0-9]run collapsed to-, length-capped), so/,.and..cannot survive into the filename — no path traversal or sensitive-file read is reachable. This matches the existing# nosemgrepprecedent for the same class inaai_cli/app/transcribe/batch.py.
Generated by Claude Code
| except NotAuthenticated: | ||
| raise | ||
| except CLIError as exc: | ||
| output.emit_warning(f"{source}: {exc.message}", json_mode=json_mode) |
There was a problem hiding this comment.
Emits a warning containing the raw 'source' and exception message (f"{source}: {exc.message}") — avoid including unsanitized user input in logs/UI.
Details
✨ AI Reasoning
A new warning emission includes the raw 'source' (user path/URL) and exception message in a formatted string passed to output.emit_warning. Both values are user-controllable or derived from user-controlled input and are emitted intact to logging/UI, which risks leaking sensitive data or enabling log-injection payloads. This was introduced in the batch streaming helper.
🔧 How do I fix it?
Keep sensitive data such as emails, passwords, and tokens out of logs. When logging values tied to a user, prefer a safe identifier like a user ID over the raw input, and strip line breaks from any user-provided text you do log.
Reply @AikidoSec feedback: [FEEDBACK] to get better review comments in the future.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
…erg-7onpfp # Conflicts: # aai_cli/commands/stream/_exec.py # aai_cli/streaming/session.py # tests/test_stream_exec.py
| def _write(path: Path, text: str) -> None: | ||
| """Write ``text`` to ``path`` (the note or sidecar), as a clean CLIError on failure.""" | ||
| try: | ||
| path.write_text(text, encoding="utf-8") |
There was a problem hiding this comment.
Potential file inclusion attack via reading file - medium severity
If an attacker can control the input leading into the open function, they might be able to read sensitive files and launch further attacks with that information.
Show fix
Remediation: Ignore this issue only after you've verified or sanitized the input going into this function.
Reply @AikidoSec ignore: [REASON] to ignore this issue.
More info
There was a problem hiding this comment.
@AikidoSec ignore: False positive (re-flagged on a new commit). savedir.py only writes (write_text/rename) to paths it assembles itself — it never opens a user-supplied path for reading. The destination comes from the user's own --save-dir/--name plus, under --auto-name, an LLM-derived title passed through naming.slugify (lowercased, every non-[a-z0-9] run collapsed to -, length-capped), so /, ., and .. cannot reach the filename — no path traversal or sensitive-file read is possible. Same class already suppressed via # nosemgrep in aai_cli/app/transcribe/batch.py.
Generated by Claude Code
There was a problem hiding this comment.
✅ Based on your feedback, we ignored this issue because of the following reason:
False positive (re-flagged on a new commit).
savedir.pyonly writes (write_text/rename) to paths it assembles itself — it never opens a user-supplied path for reading. The destination comes from the user's own--save-dir/--nameplus, under--auto-name, an LLM-derived title passed throughnaming.slugify(lowercased, every non-[a-z0-9]run collapsed to-, length-capped), so/,., and..cannot reach the filename — no path traversal or sensitive-file read is possible. Same class already suppressed via# nosemgrepinaai_cli/app/transcribe/batch.py.
Generated by Claude Code
…erg-7onpfp # Conflicts: # aai_cli/streaming/session.py
Implements the post-streaming finalization for
assembly stream --save-dir: auto-naming recordings from transcript content, writing LLM-generated notes, and creating metadata sidecars.Summary
This PR completes the
--save-dirfeature by adding three finalization steps that run after streaming ends:--auto-name): Derives a short title from the transcript via the LLM and renames the provisional timestamp-only files to include that slug (e.g.,2026-06-16-143005-quarterly-review.txt).--llm+--save-dir): Writes the final LLM answer as a.mdfile alongside the transcript..aai.jsonfile with title, date, duration, speaker list, turn count, and file references — enabling rich list/browse UIs without parsing transcripts.Key Changes
New module
aai_cli/streaming/savedir.py: Core finalization logicSaveDirPlan: Immutable dataclass capturing the resolved--save-dirintentderive_title(): Calls the LLM to generate a short headline from the transcriptwrite_outputs(): Orchestrates the rename, note write, and sidecar creationCLIErrorwithsave_dir_pathtypeNew module
aai_cli/streaming/batch.py: Extracted batch streaming logicstream_batch_sources(): Drives sequential streaming of stdin sourcessession.pyto keep session focused on single-run stateUpdated
aai_cli/streaming/session.py:save_plan,_meta_lines,_meta_speakers,_capture_start,_last_answerfields to track metadata for finalization_note_meta(): Records finalized turn text and speaker labels for the sidecar_finalize_save_dir(): Callsderive_title()(when--auto-nameand transcript non-empty) andwrite_outputs()with collected metadataUpdated
aai_cli/streaming/naming.py:SavePathsrefactored from two fields (transcript,audio) to computed properties (transcript,audio,note,sidecar) derived fromdirectoryandstemSIDECAR_SUFFIXconstant (.aai.json)Updated
aai_cli/commands/stream/_exec.py:auto_nameandno_save_audioflags toStreamOptions_resolve_save_targets()now returns aSaveDirPlanas the third element--auto-nameand--no-save-audiorequire--save-dir;--auto-nameand--nameare mutually exclusiveaai_cli/streaming/batch.pyTest infrastructure:
tests/_stream_helpers.py: Shared fakes (FakeMic,RecordingMic,FakeTurn,emit_turns,FixedDatetime,DEFAULTS) used by bothtest_stream_exec.pyand new test filestests/test_streaming_savedir.py: 218 lines of unit tests forwrite_outputs,derive_title, and error handling (pure file I/O, LLM mocked)tests/test_stream_save_dir.py: End-to-end tests of--save-dirthroughrun_stream(real session + savedir, LLM mocked)tests/test_stream_exec.py:https://claude.ai/code/session_01KNx966tACLPYX4B5jkcfqp