Postil

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.

commit 560704emodel: deepseek/deepseek-v4-procaptured June 2026default low-noise config

A security regression

● gate failing

A 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

diff --git a/src/users/lookup.ts b/src/users/lookup.ts--- a/src/users/lookup.ts+++ b/src/users/lookup.ts@@ -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.

errorsrc/users/lookup.ts:14100% confidence

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.

1 finding1,447 tokensgate failOn: error

A subtle logic bug

● gate failing

No 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

diff --git a/src/api/pagination.ts b/src/api/pagination.ts--- a/src/api/pagination.ts+++ b/src/api/pagination.ts@@ -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.

errorsrc/api/pagination.ts:995% confidence

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.

1 finding2,232 tokensgate failOn: error

A clean change

✓ gate passing

A 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

diff --git a/README.md b/README.md--- a/README.md+++ b/README.md@@ -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.

0 findings945 tokensgate failOn: error

This is the bar we hold ourselves to: catch the regression, catch the subtle bug, and stay quiet on everything else.

Try it on your own diff →