Build1 publisher3 min readPublished
A sha256 sidecar now decides what the posting agent is allowed to attach
After counting 42 scheduled Bluesky posts in a week with five images, one team cut its poster's reach to a single committed directory and made the commit gate and the poster read media through the same hashing loader.
The Engineer · Build desk

What happened
- A count on 2026-09-06 found the agent had sent 42 scheduled Bluesky posts in seven days with 5 images, against the owner's target of one post in every two carrying an image or a video.
- A stock row now names a bare filename that resolves inside one directory, content/posts-media/, and a path separator, a .., or an absolute path fails before the code touches disk.
- Each renderer output is committed with a sidecar file holding the spec it came from and a sha256 of the bytes, and only files that pass that check can be attached to a post.
- The commit gate sorts queued rows by planned time and fails on any adjacent pair of text-only posts, printing both texts so the author can see which rows need a card.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- exposure Under the old design every file on the machine was one mistyped JSON line away from a live feed, and the team treats publishing as irreversible, so a screenshot posted at 01:00 stays posted.
- cost Assets now have to come out of the renderer: a hand-cropped PNG dropped into the media directory costs a re-render before it can ship, because the hash on disk will not match the sidecar.
- decision Sharing one loader between the gate and the poster settles which side is authoritative, and any team that keeps two separate checks is choosing to find the disagreement on the runner.
- capability Encoding the target as an adjacency rule lets the gate reject a queue for the order of its rows, which a per-week ratio would have waved through.
The containment check runs twice. After the resolver has accepted a bare filename, the code confirms that the resulting absolute path still starts with content/posts-media/, so a filename that gets through a hole in the first test is caught by the second [10]. The extension has to match the declared kind as well, .png for an image and .mp4 for a video [9]. The build report calls two checks for one property deliberate, and cheap to over-enforce [10].
The directory then needed an exception to the repo's own ignore rules. content/**/*.png, .jpg, .webp and .zip are ignored everywhere, because product covers and packaged ZIPs should not be in the repository [11]. Two negation lines put this one directory back, sidecars and captions included [12]. The commit gate asserts those lines are still present, since a tidy-up that removed them would leave every media reference pointing at a file that is not in git, and on the runner the poster would find nothing at that path [13].
Committing a file does not prove a renderer made it. So each rendered output gets a sidecar, <name>.<ext>.meta.json, holding the spec it was rendered from and a sha256 of the bytes [14]. One published example describes a 56,747-byte MP4 at 1280x720, 5.2 seconds long, rendered at 2026-09-05T14:49:58.653Z from a terminal scene running pnpm push-main [15]. A single function, loadPostMedia, reads every media reference [16]. It throws when the file is missing, when the sidecar is missing, when the sidecar's kind or file disagrees with the stock row, and when the hash of the bytes on disk does not match, and the last error says to run the renderer again [17].
Both the commit gate and the poster call that function [16]. Two implementations would drift, and the report names the failure it was avoiding: a commit that passes and a post that fails at 01:00 JST on a GitHub Actions runner with nobody watching [18].
The counting rule took more thought than the hashing. The owner's phrasing was "one in two" [3], and the direct encoding is a ratio over the queue, which the team rejected because a ratio is satisfied by a queue that is all images for three days and all text for the next three [19]. The volume makes the demand concrete: 21 of the 42 posts in that week would have needed media, 5 had it, so the queue was 16 cards short, a little over two a day [22]. The 12% is a before number, and the report does not include a count from a week under the gate [24].
For any of this to transfer, every attachable asset has to be producible from a spec in the repository, which is why the team had to make its video pipeline deterministic first [21]. In a project where a human-supplied photograph is a legitimate attachment, the hash rule blocks it, and it would need a second intake path that writes its own sidecar.
What to watch
- Whether the first queue the gate blocks gets fixed by rendering a card or by relaxing the adjacency rule.
- Whether the sidecar grows a renderer version field, so a renderer change invalidates old hashes instead of passing them.
- Whether the single-directory rule holds the first time a human-supplied photograph is a legitimate attachment.