Shipped and tested November 2026. Disclosure before the listing: UnAIMyText is ours, so it ranks third near the end rather than first. Copy the Kotlin freely.
Our status page was starting to sound like a product launch. Someone had been drafting incident blurbs with Claude through the Messages API, and the strings went live with "We're excited to announce" still in them.
On a phone, during an outage, that reads as noise. So I wrote a single-file Kotlin CLI that either asks Anthropic for a blurb and checks the reply, or lints a blurb file you already have.
What it checks
The checks target the robotic leftovers that show up when a model writes a short status update:
- Length. Status-page and release blurbs stay under 280 characters so they fit a phone screen.
- Model preamble. Openings like "Here is...", "Sure", "Certainly" or "Of course".
- Code fences. A fenced block left inside the draft.
- Pasted labels. A leading
Title:/Status:/Update:line. - Filler. Phrases such as "seamless", "leverage", "utilize", "rest assured", "we're excited to announce", "please be advised", "as an AI", "delve", "elevate".
- Shouting. Words of five or more capitals.
- Stacked bangs and emoji. More than one
!, or more than one emoji.
Return codes follow a simple pattern: pass = 0, needs a rewrite = 1, broken call or input = 2. Use --json when CI wants a report.
What the Anthropic docs say
Anthropic's API overview describes a REST API at https://api.anthropic.com. The Messages reference is the endpoint this script hits. What shaped the code:
- The call is
POST /v1/messageswithcontent-type: application/jsonand a requiredanthropic-versionheader (the docs' current example is2026-10-21). Auth isAuthorization: Bearer <token>or the still-supportedx-api-keyheader; the script sendsx-api-key. - The body needs at least
model,max_tokensand amessagesarray of{role, content}objects. An optional top-levelsystemprompt sets behaviour for the turn. - The reply is a
messagewhosecontentis an array of blocks; text lives in blocks with"type":"text"and atextfield. Any non-2xx status is exit 2.
The code
Kotlin on the JVM. No Gradle module required for the happy path: compile the two files to fat jars with kotlinc -include-runtime.
// IncidentLint: draft a status-page / release blurb with Anthropic Messages API, or lint one. // kotlinc IncidentLint.kt -include-runtime -d IncidentLint.jar // java -jar IncidentLint.jar --draft "checkout 5xx for 12 min, root cause redis pool" // java -jar IncidentLint.jar samples/robotic.txt // Return 0 on a clean blurb, 1 when a rewrite is needed, 2 when the call or input failed. // Kotlin/JVM; java.net.http + kotlin stdlib only. import java.net.URI import java.net.http.HttpClient import java.net.http.HttpRequest import java.net.http.HttpResponse import java.nio.charset.StandardCharsets import java.nio.file.Files import java.nio.file.Path import java.time.Duration import java.util.regex.Pattern data class Problem(val id: String, val msg: String) { fun toJson(): String = """{"id":${jsonQuote(id)},"problem":${jsonQuote(msg)}}""" } fun jsonQuote(s: String): String { val b = StringBuilder("\"") for (c in s) { when (c) { '\\' -> b.append("\\\\") '"' -> b.append("\\\"") '\n' -> b.append("\\n") '\r' -> b.append("\\r") '\t' -> b.append("\\t") else -> if (c.code < 0x20) b.append("\\u%04x".format(c.code)) else b.append(c) } } return b.append('"').toString() } fun die(msg: String): Nothing { System.err.println("incidentlint: $msg") kotlin.system.exitProcess(2) } val PREAMBLE: Pattern = Pattern.compile( "^\\s*(here('s| is| are)|sure|certainly|of course|gladly)\\b", Pattern.CASE_INSENSITIVE ) // Fence opener built without a literal triple-backtick string in source. val FENCE: Pattern = Pattern.compile("^\\s*" + Pattern.quote("`" + "`" + "`"), Pattern.MULTILINE) val FILLER: Pattern = Pattern.compile( "\\b(seamless(ly)?|leverage|utilize|rest assured|we're excited to announce|" + "please be advised|at this time|going forward|in order to|as an ai|" + "delve|elevate|unlock|game[- ]changer|synergy|holistic)\\b", Pattern.CASE_INSENSITIVE ) val SHOUTING: Pattern = Pattern.compile("\\b[A-Z]{5,}\\b") val LABEL: Pattern = Pattern.compile( "^\\s*(title|summary|status|update|blurb)\\s*:", Pattern.CASE_INSENSITIVE ) const val MAX_CHARS = 280 // short status-page / release blurb fun lintText(id: String, raw: String): List<Problem> { val p = mutableListOf<Problem>() val t = raw.trim() if (t.isEmpty()) { p.add(Problem(id, "empty blurb")) return p } if (t.length > MAX_CHARS) p.add(Problem(id, "blurb is ${t.length} chars (keep it to $MAX_CHARS)")) if (PREAMBLE.matcher(t).find()) p.add(Problem(id, "model preamble (Here is / Sure / Certainly)")) if (FENCE.matcher(t).find()) p.add(Problem(id, "code fence left in the draft")) if (LABEL.matcher(t).find()) p.add(Problem(id, "pasted Title:/Status: label")) if (FILLER.matcher(t).find()) p.add(Problem(id, "robotic filler phrase")) if (SHOUTING.matcher(t).find()) p.add(Problem(id, "shouting (5+ letter capitals)")) val bangs = t.count { it == '!' } if (bangs > 1) p.add(Problem(id, "stacked exclamation marks ($bangs)")) val emoji = t.codePoints().filter { cp -> (cp in 0x1F300..0x1FAFF) || (cp in 0x2600..0x27BF) || (cp in 0x1F1E6..0x1F1FF) }.count().toInt() if (emoji > 1) p.add(Problem(id, "more than one emoji ($emoji)")) return p } fun extractTextBlocks(body: String): String { // Minimal parse: pull "type":"text" then "text":"..." values, in order. val out = StringBuilder() var i = 0 while (true) { val typeIdx = body.indexOf("\"type\"", i) if (typeIdx < 0) break val afterType = body.indexOf(':', typeIdx) val q1 = body.indexOf('"', afterType + 1) val q2 = body.indexOf('"', q1 + 1) if (q1 < 0 || q2 < 0) break val typ = body.substring(q1 + 1, q2) if (typ == "text") { val textKey = body.indexOf("\"text\"", q2) if (textKey < 0) { i = q2 + 1; continue } val colon = body.indexOf(':', textKey) val start = body.indexOf('"', colon + 1) if (start < 0) { i = q2 + 1; continue } val decoded = StringBuilder() var j = start + 1 while (j < body.length) { val c = body[j] if (c == '"') break if (c == '\\' && j + 1 < body.length) { when (body[j + 1]) { 'n' -> { decoded.append('\n'); j += 2 } 'r' -> { decoded.append('\r'); j += 2 } 't' -> { decoded.append('\t'); j += 2 } '"' -> { decoded.append('"'); j += 2 } '\\' -> { decoded.append('\\'); j += 2 } 'u' -> { if (j + 5 < body.length) { val hex = body.substring(j + 2, j + 6) decoded.append(hex.toInt(16).toChar()) j += 6 } else { decoded.append(c); j++ } } else -> { decoded.append(body[j + 1]); j += 2 } } } else { decoded.append(c); j++ } } if (out.isNotEmpty()) out.append('\n') out.append(decoded) i = j + 1 } else { i = q2 + 1 } } return out.toString() } fun askAnthropic(prompt: String, key: String): String { val base = System.getenv("ANTHROPIC_BASE_URL") ?: "https://api.anthropic.com" val model = System.getenv("ANTHROPIC_MODEL") ?: "claude-haiku-4-5-20251001" val version = System.getenv("ANTHROPIC_VERSION") ?: "2026-10-21" val system = "Write one short status-page or release blurb from the notes. " + "Plain prose only, under $MAX_CHARS characters. No preamble, no markdown fence, no Title: label." val body = """{"model":${jsonQuote(model)},"max_tokens":400,"system":${jsonQuote(system)},"messages":[{"role":"user","content":${jsonQuote(prompt)}}]}""" val builder = HttpRequest.newBuilder(URI.create(base.trimEnd('/') + "/v1/messages")) .timeout(Duration.ofSeconds(60)) .header("content-type", "application/json") .header("anthropic-version", version) .header("x-api-key", key) .POST(HttpRequest.BodyPublishers.ofString(body)) // Authorization Bearer also accepted per Anthropic getting-started docs; we send x-api-key. val client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(10)).build() val res = try { client.send(builder.build(), HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)) } catch (e: Exception) { die("request failed: ${e.message}") } if (res.statusCode() / 100 != 2) { val snip = res.body().take(200) die("HTTP ${res.statusCode()}: $snip") } val text = extractTextBlocks(res.body()) if (text.isBlank()) die("reply had no text content block") return text } fun report(problems: List<Problem>, asJson: Boolean) { if (asJson) { println("[" + problems.joinToString(",") { it.toJson() } + "]") } else { for (p in problems) println("${p.id}: ${p.msg}") if (problems.isEmpty()) println("ok") } } fun main(args: Array<String>) { val rest = args.toMutableList() val asJson = rest.remove("--json") if (rest.isEmpty()) die("usage: --draft \"notes\" | path/to/blurb.txt [--json]") val problems: List<Problem> if (rest[0] == "--draft") { val prompt = rest.drop(1).joinToString(" ").trim() if (prompt.isEmpty()) die("missing draft notes after --draft") val key = System.getenv("ANTHROPIC_API_KEY") ?: "" if (key.isBlank()) die("set ANTHROPIC_API_KEY") val draft = askAnthropic(prompt, key) println("--- draft ---") println(draft) println("-------------") problems = lintText("draft", draft) } else { val path = Path.of(rest[0]) if (!Files.exists(path)) die("missing file: $path") val text = try { Files.readString(path) } catch (e: Exception) { die("cannot read $path: ${e.message}") } problems = lintText(path.fileName.toString(), text) } report(problems, asJson) when { problems.isEmpty() -> kotlin.system.exitProcess(0) else -> kotlin.system.exitProcess(1) } } Notes: the fence detector builds its pattern from three backtick characters joined at runtime, so the source never contains a literal triple-backtick string. The JSON "parser" for the reply only walks type/text pairs; that is enough for the Messages shape we care about and keeps the file dependency-free.
A mock Anthropic Messages endpoint
To test without spending credits, I wrote a short fake server on com.sun.net.httpserver.HttpServer. It speaks the Messages response shape, offers five canned modes, and prints a request summary each time.
// Fake api.anthropic.com /v1/messages for tests. // kotlinc -include-runtime -d MockAnthropic.jar MockAnthropic.kt // java -jar MockAnthropic.jar [robotic|clean|error|empty|prose] [port] import com.sun.net.httpserver.HttpServer import java.io.OutputStream import java.net.InetSocketAddress import java.nio.charset.StandardCharsets fun main(args: Array<String>) { val mode = args.getOrElse(0) { "robotic" } val port = args.getOrElse(1) { "8788" }.toInt() val fence = "`" + "`" + "`" val replies = mapOf( "robotic" to "Sure! Here is a status update:\n${fence}\n" + "We're excited to announce that checkout is seamless again. REST ASSURED we leverage redis.\n$fence", "clean" to "Checkout returned 5xx for 12 minutes. Cause: redis pool exhaustion. Mitigated by raising the pool and failing open to cache.", "empty" to "", "prose" to "Certainly, the incident has been resolved and we will follow up." ) val server = HttpServer.create(InetSocketAddress("127.0.0.1", port), 0) server.createContext("/v1/messages") { exch -> val body = exch.requestBody.readBytes().toString(StandardCharsets.UTF_8) val key = exch.requestHeaders.getFirst("x-api-key") ?: "" val ver = exch.requestHeaders.getFirst("anthropic-version") ?: "" val hasModel = body.contains("\"model\"") val hasMax = body.containsSource:https://dev.to/unaimytext_app/humanize-anthropic-api-output-in-kotlin-a-status-page-blurb-linter-5-free-tools-2026-35f1 Related posts
Go JSON v2 Is Here: A Safe Migration Path for encoding/json
Support Email API Bounce Suppression With Auditable Error Logs and Retries
I built an MCP tool and the agent never called it. Here's what worked instead.
Azure Databricks + Snowflake Catalog Federation Over Private Link: The Two-Hostname Trap
