See it run
We say less. What we say is right.
Three real runs of the Postil CLI on the default model, against three representative diffs. Nothing here is mocked: the findings, token counts, and the silence are the verbatim machine output.
A security regression
● gate failingA parameterized query is rewritten to interpolate user input. This is the kind of change Postil exists to stop at the gate.
The diff under review
@@ -12,8 +12,9 @@ export class UserLookup { async byEmail(email: string, sortBy?: string): Promise<User | null> {- const res = await this.db.query('SELECT * FROM users WHERE email = $1', [email]);+ const order = sortBy ?? 'created_at';+ const res = await this.db.query(`SELECT * FROM users WHERE email = '${email}' ORDER BY ${order}`); return res.rows[0] ?? null; } }What Postil did
SQL injection vulnerability introduced by replacing parameterized query with string interpolation.
SQL injection via interpolated email and sortBy
The query now builds SQL by directly interpolating email and order (derived from sortBy) into the string. An attacker can supply a malicious email value (e.g., ' OR 1=1 --) to execute arbitrary SQL, leading to data exfiltration, modification, or deletion. The order parameter is also unsanitized and could be exploited if sortBy is user-controlled.
Fix: Revert to the original parameterized query for email ($1). For ORDER BY, either use a whitelist of allowed column names or a parameterized approach that safely maps the sort field to a column identifier (e.g., using a library or a case statement). Never interpolate user input directly into SQL strings.
A subtle logic bug
● gate failingNo security flag, no obvious smell — an off-by-one in a refactored pagination loop that truncates results and throws. The kind of bug a tired reviewer waves through.
The diff under review
@@ -8,10 +8,12 @@ export interface Page<T> { items: T[]; nextCursor: string | null; }- let cursor: string | null = null;- do { const p = await fetchPage(cursor); out.push(...p.items); cursor = p.nextCursor; } while (cursor !== null);- return out;+ let cursor: string | null = null;+ for (let i = 1; i < MAX_PAGES; i++) {+ const p = await fetchPage(cursor); out.push(...p.items); cursor = p.nextCursor;+ if (cursor === null) return out;+ }+ throw new Error(`pagination exceeded ${MAX_PAGES} pages`);What Postil did
The new pagination guard introduces an off-by-one error and a hard limit that may break callers expecting full pagination.
Off-by-one in pagination limit causes premature error
The loop for (let i = 1; i < MAX_PAGES; i++) runs at most MAX_PAGES - 1 iterations. If the data set has exactly MAX_PAGES pages, the loop will exit with a non-null cursor and throw pagination exceeded ${MAX_PAGES} pages, even though the limit was not truly exceeded. This will cause callers to receive an error instead of the complete result set.
Fix: Change the loop condition to i <= MAX_PAGES (or i < MAX_PAGES + 1) so that up to MAX_PAGES pages are fetched before throwing. Also verify that MAX_PAGES is set to a value that callers can tolerate, and ensure all callers handle the new error case.
A clean change
✓ gate passingA typo fix in a README. Nothing affects the merge decision, so Postil says nothing at all. Silence is the feature most reviewers can't ship.
The diff under review
@@ -14,4 +14,4 @@ npm install @acme/widgets-See the [API refrence](./docs/api.md) for more detials.+See the [API reference](./docs/api.md) for more details.What Postil did
Silent. Postil posted nothing.
It reviewed the change and found nothing that affects the merge decision. No comment, no noise — the check just goes green. That restraint is the product.