Build1 publisher3 min readPublished
An Obsidian vault pipeline re-validates JSON from a model stripped of write tools
Every 10 minutes an unattended job summarizes Claude Code and Codex logs into an Obsidian vault and pushes the commit, so the pipeline treats the summarizer's own JSON as text that may carry pasted keys or injected instructions.
The Engineer · Build desk

What happened
- Every 10 minutes an unattended job has a model summarize the author's AI conversation logs, writes the summary to AI/SESSION-STATE.md, and runs git commit and push with no human review.
- Responses must conform to a JSON schema passed on the command line, and a non-conforming response makes the call fail rather than emit free-form text.
- The prompt separately forbids including or paraphrasing credentials, API keys, tokens, passwords, cookies and auth URLs, among other categories of private data.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure The trust boundary sits at the log directory, not at the model: anything pasted into a Claude Code, Codex or manus transcript is a candidate for the next summary.
- constraint The artifact is a pushed git commit that later sessions read as memory, so one bad run is durable and there is no reviewer standing between generation and history.
- decision Anyone wiring an agent into a memory store has to decide where validation lives; this design puts it after generation, on the grounds that tool flags constrain actions and not content.
- cost Running writes unattended costs a second program to maintain alongside the launcher script, plus a schema and a prompt ban list that the author says the model may ignore.
The flag I would copy first is `--strict-mcp-config --mcp-config '{"mcpServers":{}}'`. It pins the MCP server list to empty in the invocation, so the run does not depend on whether the config file on disk happens to be clean that day [10]. The allowlist is `--tools 'Read,Grep,Glob'`, which leaves out Bash, Edit, Write and MCP [9]. The model can read the logs; it cannot write the file the summary lands in [9]. The call also runs `--print --no-session-persistence --model sonnet` [23].
That bounds what the process can do, not what the text can say. The author of the dev.to post is explicit about the split: even with tools limited that way, the post says, "you can't control the content of the text the model returns" [8]. So the output is checked again on arrival, in `normalize-memory-reflection.py`, which the author calls zero-trust [24]. The pipeline is two files, that gate and the launchd-driven `memory-reflect.sh` [7].
The schedule leaves room for the timeout. A 10-minute interval is 600 seconds and `MODEL_TIMEOUT_SECONDS` is 120 [12], so a hung call is killed with 480 seconds of slack before the next run is due [21]. At that spacing the job fires 144 times a day [22].
The schema is where the response stops being prose. Required top-level keys are `session_state`, `proposals`, `sources_reviewed` and `limitations`, with `additionalProperties` false [14]. `session_state` is capped at 5000 characters, `proposals` at 8 items, `sources_reviewed` at 16, `limitations` at 12 [15]. Inside a proposal, the listing in the post gives types for `kind` and `confidence` only, both enums, and the required `title` and `content` fields appear with no length limit [16]. The schema therefore bounds how many proposals a run emits, and the published listing does not bound how long each one is.
The prompt carries a second layer: never include or paraphrase credentials, API keys, tokens, passwords, cookies, auth URLs or codes, among other categories [17]. The author's own caveat is that this is "only a request to the model" [18]. It asks the component that just read the key not to repeat it. If the Claude call fails, the script tries a local Qwen fallback [19]; the available text names the two pipeline files but does not include the regex patterns or say whether the fallback output is schema-checked [7].
For this design to transfer, the risk you care about has to live in the returned text. Here it does: an API key or auth URL pasted into a log, or injection text that arrived through another AI or a web page, written into git history where the next session reads it as memory [5][6]. If your agent's risk is instead that it can shell out or edit files, the allowlist is the part that carries over, and a regex gate on its prose buys you little.
What to watch
- Whether a follow-up publishes the three gate locations and the actual regex patterns inside normalize-memory-reflection.py.
- Whether the local Qwen fallback path is held to the same JSON schema, given that --json-schema is a Claude CLI flag.
- Whether the proposals item schema gains maxLength on title and content, which would bound a run's byte size.