I built a free AI-powered Git CLI that writes your commit messages for you

We’ve all been there. You just spent 3 hours fixing a bug, you stage your changes, and then you stare at the commit message input like it owes you money. You end up typing fix stuff and moving on.

I got tired of it. So I built gritch.

What is gritch?

gritch is a free, open source CLI tool that uses Groq’s free AI API (LLaMA 3.3) to help you write better commits, review your code before pushing, generate changelogs, and understand old commits all from your terminal.

What it can do

Generate a conventional commit message from your staged changes:

git add .
gritch commit

Review your code before pushing:

gritch review
gritch review --language javascript

Explain any commit in plain English:

gritch explain abc1234

Generate a changelog between two versions:

gritch changelog v1.0.0 v1.1.0

How to get started

Install it globally:

npm install -g gritch

Get a free API key from https://console.groq.com and set it:

Windows:

$env:GROQ_API_KEY="your_key_here"

macOS / Linux:

export GROQ_API_KEY="your_key_here"

Then run gritch in any Git project.

Why I built it

I wanted to build something that developers actually use daily not another todo app or weather dashboard. gritch solves a real problem I had every single day, and I built it entirely with TypeScript and Node.js with zero paid services required.

Links

If you find it useful, a star on the repo goes a long way. _

OpenLiDARViewer: A Browser-Based LiDAR and Point-Cloud Viewer

Rendering LiDAR Scans in the Browser Without Uploading Anything

Most point-cloud workflows still start the same way.

Install a desktop tool.
Import the scan.
Wait.
Inspect.
Measure.
Export.

That workflow makes sense when you are doing serious GIS, photogrammetry, survey processing, classification, or production work.

But there is another moment that is much simpler:

You just received a scan and want to know what is inside it.

Does it open?
Is it clean?
Does it have color, intensity, or classification?
Can I measure something quickly?
Can I show it to someone without making them install software?

That is the moment I wanted to improve with OpenLiDARViewer.

It is an open-source, browser-based LiDAR and point-cloud viewer built around one simple idea:

Drop a scan into the browser and inspect it locally.

No upload.
No account.
No desktop install.
No conversion step for supported formats.

Live demo: https://lidar.aurtech.mx/
GitHub: https://github.com/Aurtechmx/openlidarviewer/

The real goal: own the first 60 seconds

There are already great tools for point-cloud work.

CloudCompare is powerful.
QGIS is powerful.
Potree is excellent for publishing point clouds on the web.
Professional LiDAR software exists for a reason.

OpenLiDARViewer is not trying to replace those tools.

It is focused on a smaller, earlier step:

the first 60 seconds after you get a scan.

Before you process it.
Before you classify it.
Before you prepare a report.
Before you decide what workflow it belongs to.

Sometimes you just need a fast first look.

That sounds simple, but LiDAR data makes “just open it” surprisingly complicated.

Keeping the scan on the device

One thing I wanted from the start was simple:

The scan should not have to leave the user’s machine just to be inspected.

For LiDAR and 3D scan data, that matters.

A scan can represent a private site, a client project, a building, a terrain model, an industrial asset, or a research dataset. Uploading it to a random cloud viewer just to take a quick look is not always acceptable.

So OpenLiDARViewer runs client-side.

The browser loads the app, but the scan itself is read, parsed, analyzed, and rendered locally.

There is no backend parser.
No server-side preprocessing.
No cloud ingestion pipeline.
No “upload your file and wait.”

Just the browser, the GPU, and the file on your machine.

Supporting real-world scan formats

The viewer now supports common point-cloud and scan formats such as:

  • LAS
  • LAZ
  • E57
  • PLY
  • OBJ
  • GLB / GLTF
  • XYZ
  • CSV

That mix is intentional.

I wanted the same viewer to handle both:

  • drone / aerial LiDAR
  • phone and mobile 3D scans

Those worlds often feel separate, but in practice they are starting to overlap. People capture data from drones, phones, scanners, photogrammetry tools, and AR apps. A lightweight viewer should not care where the scan came from as long as the data can be parsed and rendered.

E57 support was especially important because it is a serious professional exchange format. It also brings more complexity: metadata, scan structure, transforms, optional attributes, and vendor differences.

Getting that kind of file working in the browser is not just a checkbox. It is an interoperability milestone.

Measurement had to become more than a demo feature

The first measurement tool was simple: pick two points and get a distance.

Useful, but limited.

The newer measurement workflow is more practical. It includes:

  • distance
  • polyline
  • area
  • height
  • angle
  • slope
  • unit switching
  • editable points
  • clearer measurement sessions

It does make it more useful for quick inspection.

Scan intelligence: not just seeing the cloud

A point cloud viewer should do more than show dots.

When I open a scan, I want quick answers:

  • How many points are in this file?
  • What is the extent?
  • What is the approximate density?
  • Does it have RGB?
  • Does it have intensity?
  • Does it have classification?
  • Are there invalid coordinates?
  • Are there suspicious outliers?
  • Does the decoded data match what the file claims?

That is why OpenLiDARViewer includes scan intelligence and validation modules.

The goal is not to replace professional QA/QC.

The goal is to give a useful first read before committing to deeper processing.

Seeing the scan matters.

Understanding whether the scan looks intact matters too.

Why the interface is more game-like than GIS-like

A lot of spatial tools inherit GIS interaction patterns.

That is fine for GIS users, but it can be intimidating for people who just want to move through a 3D scan.

OpenLiDARViewer uses a more direct navigation model:

  • Orbit mode
  • Walk mode
  • Fly mode
  • WASD movement
  • mouse-look
  • touch support on mobile
  • point inspection
  • saved viewpoints

The goal is to make the scan feel like a space you can enter, not just a dataset you loaded.

Orbit works well for objects and small sites.
Walk makes more sense for interiors or street-level scans.
Fly feels better for terrain, drone LiDAR, and wide-area data.

This is one of the parts I care about most because good interaction design can make technical data much easier to understand.

Mobile was not optional

At first, this kind of tool feels desktop-first.

Then reality shows up.

People want to open scans:

  • on site
  • on tablets
  • on phones
  • during a demo
  • while moving between field and office workflows

Mobile support creates its own set of problems:

  • touch controls
  • viewport behavior
  • limited memory
  • smaller screens
  • browser gestures
  • GPU differences
  • file picker behavior

A viewer that technically opens on mobile but feels terrible is not really mobile-friendly.

So mobile support became part of the direction, especially for phone scan exports and quick review workflows.

Feedback I am looking for

If you work with any of these areas, I would really value your feedback:

  • LiDAR
  • UAV mapping
  • GIS
  • photogrammetry
  • 3D scanning
  • WebGL / WebGPU
  • point-cloud visualization
  • browser-based technical tools

The most useful feedback would be:

  • Does your file open?
  • Does performance feel acceptable?
  • Are the controls intuitive?
  • Are the measurement tools useful?
  • Does E57 behave correctly with your files?
  • What format support is missing?
  • What breaks?
  • What would make this useful in a real workflow?

Live demo: https://lidar.aurtech.mx/
GitHub: https://github.com/Aurtechmx/openlidarviewer/

If the project is useful, a GitHub star helps. But real feedback from people who work with point-cloud data helps even more.

Final thought

The browser is becoming a serious runtime for technical software.

Not for everything.

Not for every workflow.

But for the first step — opening, inspecting, measuring, and understanding spatial data — it is starting to make a lot of sense.

That is the direction OpenLiDARViewer is exploring.

Why most freelancers undercharge (and the maths behind fixing it)

Most freelancers set their rate by looking at what others charge and picking something nearby. This feels safe. It also creates a systematic problem: you end up pricing based on what the market accepts, not what your business actually needs.

Here is the maths that most rate-setting advice skips.

The utilisation problem

If you work 40 hours a week, you are not billing 40 hours a week.

A realistic breakdown looks something like this:

  • Client calls and emails: 4-6 hours
  • Proposals and pitching: 3-5 hours
  • Admin, invoicing, accounting: 2-3 hours
  • Learning and keeping skills current: 2-4 hours

That leaves roughly 22-28 billable hours out of 40. Call it 55-65% utilisation.

A developer billing $40/hr with 20 real billable hours per week is effectively running a ~$40k/year business before taxes and downtime. That number tends to surprise people when they see it written out.

So if you want to earn $80,000 a year, you cannot divide by 2,000 hours and call it your rate. You need to divide by your actual billable hours.

$80,000 / 1,200 billable hours = $66.67/hr minimum

Before taxes. Before slow months. Before the occasional client who costs you two weeks of work and pays nothing.

The slow month multiplier

Freelance income is not linear. If you plan for 48 weeks of full utilisation and you actually get 38, your effective rate just dropped 21% below what you calculated.

The fix is to build slow months into your rate from the start, not to adjust when they happen.

A more conservative model:

  • Target: 46 billable weeks per year (not 52)
  • Utilisation: 60% of working hours
  • Effective billable hours: 46 x 40 x 0.60 = 1,104 hours

For an $80,000 annual target: $80,000 / 1,104 = $72.46/hr before tax

Add 25-30% for self-employment tax and the minimum viable rate is around $90-95/hr. Depending on your market and experience level, that number may feel impossibly high. That’s exactly why so many freelancers end up overworked and underpaid — the maths doesn’t change because the market is uncomfortable with it.

Why this matters for developers specifically

Frontend and backend developers are particularly prone to undercharging because the market has a wide range and clients often anchor to offshore rates.

That anchor is irrelevant to your cost structure. An offshore developer at $25/hr has different fixed costs, tax obligations, and market positioning. Comparing your rate to theirs is like a restaurant comparing its prices to street food in a different country.

Your rate is a function of your costs and your utilisation. Not of what someone else charges somewhere else.

A simple starting framework

  1. Calculate your target annual income (include savings, not just survival)
  2. Estimate your real billable hours (utilisation x working weeks x hours/week)
  3. Divide income by hours to get your pre-tax minimum
  4. Add your effective tax rate
  5. Add 15-20% as a buffer for slow months and difficult clients

That number is your floor. Anything below it usually means the business only works if you absorb the volatility yourself.

I got tired of doing this maths manually every time someone asked me about freelance pricing, so I built a small calculator for different roles: ratecalculator.xyz

No signup, no email, just the numbers.

We built a mahjong dangerous-tile predictor calibrated on 4.97M real hands

The problem

At a real mahjong table, software can’t help you mid-hand. The question — “is this tile safe to discard?” — comes up every few turns, and you work it out from memory and pattern recognition alone.

We wanted something you could set next to you at the table, pick up in a second, and put down without disrupting the game.

What OkkanaiPai does

Three interactions:

  1. Swipe to select the player you want to read
  2. Tap their discarded tiles to enter them
  3. See all 34 tiles color-coded by danger level

The whole flow is designed to be glanceable — you’re not staring at a screen mid-game.

The model behind it

We didn’t ship a live inference model. Instead, we calibrated a rule-based system against 4.97 million discards from Tenhou’s Houou-takujo server (the top competitive tier), covering 16 days of logged play.

The result is a set of coefficients stored as JSON inside the app. AUC on held-out data came to 0.83. Not perfect, but statistically meaningful for a glance-level judgment call at the table. Everything runs fully offline — no network request, no account, no subscription.

What it doesn’t cover

Being upfront about scope:

  • Calibration is East-round / East-seat data. Accuracy may degrade in South-round play.
  • Meld efficiency — how chii, pon, or kan reshapes hand danger — is not modeled.
  • Three-player mahjong (sanma) is not supported.

For standard 4-player East-focused games, it fits. Outside that, weight the output accordingly.

Why build it

Niixo Labs is running a sprint: small, focused iOS tools, shipped fast. OkkanaiPai explores what “calibrated statistics as UX” looks like in a game-assistance context — where a lightweight, offline coefficient table can deliver meaningful signal without any server infrastructure. Free, no ads, no IAP.

https://apps.apple.com/jp/app/id6762544982

Building a Chord Progression Generator in the Browser — Music Theory in JS, Sound via Web Audio API

Screenshot

Pop songs, jazz turnarounds, Pachelbel’s canon. Chord progressions you’ve heard a thousand times all reduce to scale degrees stacked on a diatonic scale. This tool generates progressions in 12 keys × 2 scales × 7 genre presets and plays them through the Web Audio API. ~300 lines of vanilla JS, zero dependencies.

🌐 Demo: https://sen.ltd/portfolio/chord-progression-gen/
📦 GitHub: https://github.com/sen-ltd/chord-progression-gen

Screenshot

The minimum music theory you need

If “music theory” sounds heavy, the slice you actually need to write code is surprisingly small.

1. A scale is just 7 notes

The C major scale is C, D, E, F, G, A, B. In semitones from the root: [0, 2, 4, 5, 7, 9, 11]. That’s it.

const MAJOR_SCALE = [0, 2, 4, 5, 7, 9, 11];
const NATURAL_MINOR = [0, 2, 3, 5, 7, 8, 10];

Natural minor (C, D, E♭, F, G, A♭, B♭) is [0, 2, 3, 5, 7, 8, 10].

2. A chord stacks the 1st, 3rd, and 5th degrees (diatonic triad)

Take notes 1, 3, 5 of the scale: C, E, G → C major triad. Start from note 2 instead: D, F, A → D minor. The starting position is the “scale degree” and we label it with a roman numeral:

Degree C major Quality
I C Major
ii Dm minor
iii Em minor
IV F Major
V G Major
vi Am minor
vii° B° diminished

Uppercase = major, lowercase = minor, ° = diminished. Every chord progression in this app is a sequence of these seven.

3. A progression is just an ordered list of degrees

  • Pop: I → V → vi → IV (in C: C → G → Am → F — the “Axis of Awesome” progression)
  • Pachelbel’s Canon: I → V → vi → iii → IV → I → IV → V
  • Jazz turnaround: ii → V → I → vi

Building the chord

The triad-building function:

export function chordTones(tonic, scaleName, degree, seventh = false) {
  const scale = SCALES[scaleName];
  const positions = seventh ? [0, 2, 4, 6] : [0, 2, 4];
  return positions.map((p) => {
    const idx = degree + p;
    const octaveShift = Math.floor(idx / 7) * 12;
    return tonic + octaveShift + scale.intervals[idx % 7];
  });
}

tonic is the MIDI number of the root (C4 = 60). When degree + p walks past the seventh scale note we need to wrap to the next octave, hence Math.floor(idx / 7) * 12. Building vi (Am) in C major: chordTones(60, "major", 5) returns [69, 72, 76] — A4, C5, E5.

For seventh chords, change positions to [0, 2, 4, 6]. The chord quality (maj7 / 7 / m7 / m7♭5) we derive from the interval pattern afterwards.

Naming chords without a lookup table

We could hardcode “Am” → “minor on A”, “Bm7♭5” → “half-diminished on B” and so on. But that table has 12 keys × 7 degrees × 4 qualities × {triad, seventh} entries, and it doesn’t extend when you add modes (Dorian, Phrygian, harmonic minor).

Better: derive the name from the interval pattern of the constructed chord:

export function chordName(tonic, scaleName, degree, seventh = false) {
  const tones = chordTones(tonic, scaleName, degree, seventh);
  const rootPc = ((tones[0] % 12) + 12) % 12;
  const third = tones[1] - tones[0];
  const fifth = tones[2] - tones[0];
  let quality = "";
  if (third === 4 && fifth === 7) quality = "";      // Major
  else if (third === 3 && fifth === 7) quality = "m"; // minor
  else if (third === 3 && fifth === 6) quality = "°"; // dim
  else if (third === 4 && fifth === 8) quality = "+"; // aug
  // ... seventh logic continues
  return NOTE_NAMES[rootPc] + quality;
}

Major third (4 semis) + perfect fifth (7 semis) → Major. Minor third (3) + perfect fifth (7) → minor. Minor third (3) + diminished fifth (6) → diminished. Names fall out of the intervals, so adding Dorian or harmonic minor later doesn’t require touching the naming logic at all.

Playing it through Web Audio

The audio engine is the kind of thing that looks complicated and turns out to be 30 lines.

import { midiToHz } from "./theory.js";

class ChordPlayer {
  constructor() {
    this.ctx = null;
  }

  ensureContext() {
    if (this.ctx) return;
    this.ctx = new AudioContext();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.18;
    const filter = this.ctx.createBiquadFilter();
    filter.type = "lowpass";
    filter.frequency.value = 2400;
    this.master.connect(filter);
    filter.connect(this.ctx.destination);
  }

  scheduleChord(tones, startTime, duration) {
    for (const midi of tones) {
      const osc = this.ctx.createOscillator();
      osc.type = "triangle";
      osc.frequency.value = midiToHz(midi);
      const gain = this.ctx.createGain();
      gain.gain.setValueAtTime(0, startTime);
      gain.gain.linearRampToValueAtTime(1 / tones.length, startTime + 0.02);
      gain.gain.linearRampToValueAtTime(0, startTime + duration);
      osc.connect(gain);
      gain.connect(this.master);
      osc.start(startTime);
      osc.stop(startTime + duration + 0.05);
    }
  }
}

Four small choices matter:

1. Use triangle oscillators

sawtooth is too harsh, sine is too thin, square sounds like a Game Boy. triangle lands in the middle and reads as “acoustic-ish” without effort.

2. ADSR can be trivial

Real pianos and strings have complex envelopes. For chord progressions you don’t need them. A 20 ms attack and a short linear release is enough to avoid clicks and sound natural:

volume
 1.0 │   /‾‾‾‾‾‾‾‾‾‾‾
     │  /              
 0.0 │_/                ____
     ├──┼──────────────┼─┼──→ time
        20ms          release start

3. Divide gain by chord size

Stacking 3 or 4 oscillators at full volume sums to clipping. Multiply each oscillator’s gain by 1 / tones.length and the master stays well-behaved no matter how many voices you add.

4. Resume the context on user gesture

Chrome’s autoplay policy refuses to let AudioContext make sound until the user has interacted with the page. If you call osc.start() before ctx.resume(), the first chord goes silently into the void:

async resume() {
  this.ensureContext();
  if (this.ctx.state === "suspended") await this.ctx.resume();
}

Skip this and you get the classic “works locally, silent in production” trap.

Architecture

theory.js   ← Music theory (pure, zero DOM/Audio dependencies)
audio.js    ← Web Audio scheduler (consumes MIDI numbers from theory.js)
app.js      ← UI glue (DOM events → theory → audio)

Nothing in theory.js knows about the DOM or about AudioContext. That means Node’s built-in test runner can verify all 25 cases of the music logic without a browser:

import { test } from "node:test";
import assert from "node:assert/strict";
import { buildProgression, tonicMidi, PRESETS } from "../theory.js";

test("Pop in C major yields C G Am F", () => {
  const out = buildProgression({
    tonic: tonicMidi("C", 4),
    scaleName: "major",
    degrees: PRESETS.pop.degrees,
  });
  assert.deepEqual(out.map((c) => c.name), ["C", "G", "Am", "F"]);
});

audio.js consumes the output of theory.js (MIDI number arrays) and has no opinion about scales or roman numerals. The dependency arrow only points one direction. When you eventually add Dorian, harmonic minor, secondary dominants, or jazz substitutions, the audio layer doesn’t change at all — theory.js keeps emitting MIDI, the rest keeps playing it.

app.js is a tiny coordinator: UI event → mutate state → rebuild progression via theory → optionally play through audio. No React, no Vue, no signals. The whole state is a plain object.

Try it

Takeaways

  • Chord progressions reduce to 7 notes × scale degrees × roman numerals — small enough to fit in your head
  • Deriving chord names from interval relationships instead of a lookup table makes the music-theory module trivially extensible
  • The Web Audio API plays chord progressions cleanly with triangle waves + minimal ADSR + autoplay-policy handling — that’s the whole synthesis layer
  • Keeping music theory pure (no DOM, no audio) means node --test can verify all the harmony rules without spinning up a browser

This is OSS portfolio #240 from SEN LLC (Tokyo). We ship small, sharp tools continuously: https://sen.ltd/portfolio/

tutorial #10: 148 Opens, 0 Replies — How My Forge Cold Email v1 Completely Failed

Zenn version: https://zenn.dev/mintototo1/articles/buildlog-2026-05-23-cold-email-v2

I sent 100 emails and got 148 opens.

Someone opened multiple times. People were reading. But 0 replies.

Between May 12–14, I sent 100 cold emails through Instantly for Forge — my AI engineering placement side project. Targeted companies actively hiring AI engineers, personalized each one.

Results:

  • Opens: 148 (including multiple opens per recipient)
  • Replies: 0
  • Bookings: 0

The subject lines worked. People were reading. Nobody replied. I couldn’t figure out why. So I went back and read the body.

The problem was the offer sequence

My v1 email led with: “Freelance AI engineer placement, 30% fee, I’ll send you details.”

If you get an email from someone you’ve never heard of, and it immediately says “want to start a 30% fee arrangement? I’ll send details” — you don’t reply. That’s obvious. I just didn’t see it.

The first email should give something for free, not pitch. I skipped that step. The people who opened probably thought “not now” and closed it. That’s all it was.

What I changed in v2

Changed first-touch offer to: free 15-min audit + 1-page AI hiring roadmap.

// v1: expecting a reply
I'll send you details.
Please reach out at your convenience.

// v2: calendar booking completes the action
Let me audit your AI hiring setup in 15 minutes
and put together a 1-page roadmap.
Book here → https://cal.com/mint-office-hours/free-15min

Also cut body from 280–400 chars down to 200–320 chars.

One next action: calendar booking. No waiting for a reply.

v2 results aren’t in yet — sent this week, will write about it next week. No idea if the 0 moves.

9 in 10 Docker Compose files skip the basic security flags

Most common findings across the corpus, by share of parsed files affected, coloured by severity

I created compose-lint, a security linter for Docker Compose files, and pointed it at 6,444 public docker-compose.yml and compose.yaml files from GitHub. (More on why below.)

Three numbers stood out:

  • 91% of the files that parse have at least one security finding.
  • 68% have at least one HIGH or CRITICAL finding.
  • The same three issues top every category I looked at, including the official vendor examples people are told to copy.

I don’t read this as engineers being careless. It’s about defaults. Docker Compose ships with the hardening switched off, almost nobody turns it on, and the examples people learn from don’t either.

Why this exists

By day I lead a team of security engineers at a large financial institution, where Compose barely comes up. Production runs on Kubernetes and ECS, both with mature security tooling around them. At home in my lab, though, Compose is the right tool: quick, low-ceremony, enough to stand up a stack on a Saturday.

What bugged me was the asymmetry. Kubernetes and Terraform have a deep bench of scanners: Checkov, Trivy, kube-bench, Kubescape. Compose is an afterthought in most of them. The Compose-specific tools I found solved adjacent problems instead. Hadolint lints Dockerfiles, not Compose files. dclint checks Compose structure and style, not security.

What I wanted was simple: a zero-config, OWASP/CIS-grounded linter I could drop into CI and run against my own stacks. So I wrote one. Then I got curious whether the stuff I kept fixing in my own files showed up everywhere else.

It does. That’s what this writeup is about, and I’m putting the tool out there in case it’s useful to anyone who builds the way I do.

Personal project; the views here are my own, not my employer’s.

Here’s what I found.

How the corpus is built

I split the files into four tiers. A single “X% of Compose files do Y” number is misleading when it averages a polished Bitnami example against someone’s half-finished homelab:

  • canonical (327 files) — official upstream examples: awesome-compose, Bitnami, Grafana, Vaultwarden. The stuff READMEs tell you to copy-paste.
  • popular (3,977) — repos with ≥50 stars and a recent Compose file. Production-adjacent code.
  • selfhosted (588) — app-store / template registries: CasaOS, runtipi, Dockge. Home-LAN threat model.
  • longtail (1,552) — a stratified sweep of GitHub code search. The median file in the wild.

Every file goes through the same rule set (compose-lint 0.7.0), and every rule is grounded in the OWASP Docker Security Cheat Sheet or the CIS Docker Benchmark. The full methodology, including what the study deliberately doesn’t claim, is in the canonical report.

Finding 1: nobody flips the hardening flags

Three findings fire on roughly 90% of every file in the corpus:

Most common findings across the corpus, by share of parsed files affected, coloured by severity

  • Filesystem not read-only (read_only: true missing) — 91%
  • No capability restrictions (cap_drop: [ALL] missing) — 91%
  • Privilege escalation not blocked (no-new-privileges missing) — 90%

They’re rated MEDIUM, not CRITICAL, because each one is a missing control rather than active misuse. That’s also what makes them interesting. The Compose hardening triple is almost never set.

The fix takes about 30 seconds per service:

services:
  app:
    image: nginx:1.27@sha256:...   # pin a digest, not just a tag
    read_only: true                # CL-0007
    cap_drop: [ALL]                # CL-0006
    security_opt:
      - no-new-privileges:true     # CL-0003

Add a tmpfs: for whatever paths your app writes to and you’ve cleared the three most common findings in the corpus.

“Aren’t these just optional config choices, not real vulnerabilities?” Mostly fair, and it’s worth being exact about what a “finding” is here. The linter isn’t claiming anything was exploited. It’s flagging that a file leaves a recommended hardening control unset. Whether that matters is a judgment call that belongs to the org or the engineer, based on their context and risk tolerance. What the linter takes off your plate is the “did I even know this control existed?” part. read_only, cap_drop: [ALL], and no-new-privileges aren’t my preferences about tidy YAML; they’re named controls in the CIS Docker Benchmark and the OWASP Docker Security Cheat Sheet. A finding means the config diverges from that published baseline. Closing the gap or accepting it is your call.

Finding 2: even copy-paste vendor examples aren’t clean

You’d expect the official examples to be the hardened ones. Being copied is their entire job. They’re the cleanest tier in the corpus, and they still come in at 83%:

Share of files with at least one finding by tier: canonical 82.8%, popular 95.2%, selfhosted 100%, longtail 78.3%

A few things jump out:

  • Self-hosted app-store templates: 100%. Every single one trips at least one rule. They’re built for “works on your LAN in one click,” which in practice means exposed ports, root users, and big host mounts.
  • Popular repos aren’t better than the long tail. Stars don’t buy hardening discipline.
  • Canonical examples are config demos, not hardening exemplars. People copy them into production anyway.

That last point is most of the story. The examples teach the unhardened shape, and the shape propagates.

A fair caveat here, especially if you’re running a homelab: threat model matters. A single-user box behind a firewall and Tailscale is a different risk calculus than something exposed to the internet, and a finding is usually something to decide about rather than an emergency. Start with what actually bites. A mounted Docker socket is full host takeover whether or not you meant to expose it, so fix those first and treat the MEDIUM pile as gradual cleanup. It’s why the CI gate defaults to fail-on: high.

Finding 3: ~10% of ordinary files don’t even parse

This is the one I didn’t expect. In the long-tail tier, 9.6% of files don’t parse as a valid Compose file at all, against well under 1% everywhere else:

Parse-error rate by tier: canonical 0.6%, popular 0.7%, selfhosted 0.0%, longtail 9.6%

And it’s almost never broken YAML. It’s shape errors: people writing services as a dictionary of strings.

# What a lot of people write (does not parse):
services:
  nginx: nginx:1.27

# What Compose actually wants:
services:
  nginx:
    image: nginx:1.27

A file that doesn’t parse with a real Compose engine was never deployed by one. So these are docs snippets, tutorial follow-alongs, half-finished first drafts. None of them are getting linted before they ship, and the parse-error rate is really a map of where unreviewed config piles up.

(If you’re wondering: “long tail” here means the low-visibility mass of ordinary repos, not a statistical distribution tail.)

Why it’s all MEDIUM-heavy

One more chart, because the severity mix surprises people:

Findings by severity: MEDIUM 78.5%, HIGH 20.3%, CRITICAL 1.1%, LOW 0.0%

Nearly four out of five findings are MEDIUM. That’s the hardening-triple misses from Finding 1, which hit almost every file and are MEDIUM by design. CRITICAL findings are rarer but real: a mounted Docker socket, cap_add: ALL, a bind-mounted /. The Docker socket one alone, which is full host takeover, shows up on 6.4% of parsed files and 8% in the popular tier.

LOW is almost empty, and that’s by construction. Only one of compose-lint’s 21 rules is LOW (a healthcheck someone explicitly turned off), because the tool’s whole scope is security misconfiguration, where the floor is MEDIUM. So read “0.0% LOW” as a fact about the tool, not as the small stuff being fine.

So why are all the flags off by default?

Because Docker optimizes for “it runs the first time.” A writable filesystem, the full capability set, privilege escalation left on: that’s the path of least surprise. Your container starts, your app works. Hardening is opt-in, and opting in means knowing the control exists, confirming your app still works without that capability or that write access, and adding a few lines per service.

And “knowing it exists” is the hard part. There’s no single secure-Compose baseline to copy from. The controls are scattered across the Compose spec, the Docker run reference, the OWASP Docker Security Cheat Sheet, and the CIS benchmark. You’d have to already know that no-new-privileges is a thing, that cap_drop: [ALL] goes before a targeted cap_add, that read_only: true usually needs a tmpfs for whatever your app writes. Most people writing a Compose file aren’t container-security specialists. They want their stack up. Expecting everyone to carry that whole surface in their head is how you end up with a 91% finding rate. A linter flips it around: you don’t memorize anything, you fix the line it points at and read why.

And it compounds. The examples never opt in, so the next person copies the unhardened shape, ships it, and becomes the next example someone copies. The corpus is that loop at scale. None of it is exotic. It’s the accumulated weight of a sensible default that nobody goes back to revisit.

What this is not

A few things I’m explicitly not claiming, because it’s easy to over-read this:

  • It measures how common misconfigurations are, not whether they were exploited. A finding is divergence from hardening guidance, nothing more.
  • It’s descriptive sampling, not statistical inference. No p-values, no population estimates.
  • It’s GitHub-only and public-only. Private and enterprise Compose may look different.

The full “what this study does not claim” section in the report lays out every boundary.

Try it on your own files

compose-lint is MIT-licensed, zero-config, and depends only on PyYAML. A few ways to run it:

# one-off, locally
pipx install compose-lint && compose-lint docker-compose.yml

# or the published image (distroless, nonroot)
docker run --rm -v "$(pwd):/src" composelint/compose-lint

In CI, there’s a GitHub Action:

# .github/workflows/compose-lint.yml
name: compose-lint
on: [pull_request]
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: tmatens/[email protected]
        with:
          pattern: "**/*compose*.y*ml"   # docker-compose.yml, compose.yaml, …
          fail-on: high

fail-on: high (the default) fails only on HIGH/CRITICAL, so you can adopt it without drowning in the MEDIUM backlog on day one, then tighten later. There’s also a pre-commit hook, JSON and SARIF output (SARIF feeds GitHub Code Scanning), and compose-lint --explain CL-0007 to print any rule’s rationale and fix.

For what it’s worth on a tool you’d wire into CI: every rule cites OWASP, CIS, or Docker docs, the image is distroless and nonroot, and releases ship SLSA provenance and Sigstore attestations. Details are in the repo.

The full report has every table, the complete methodology, the per-rule breakdowns, and steps to reproduce it: State of Docker Compose Security

If you maintain a popular Compose example, I’d genuinely love a PR or an issue. Hardening the examples people copy is the highest-leverage fix there is.

How to Forward Android SMS to Telegram Automatically

Bank codes, server alerts, delivery updates and team OTP messages still arrive on one physical Android phone.

If that phone sits in an office drawer, a courier bag or another country, the whole team waits.

This guide compares five practical ways to forward Android SMS to Telegram, from free APKs to Tasker, n8n and a cloud SMS forwarder.

TL;DR: best options

Method Best for Setup time Main trade-off
Cloud SMS forwarder Teams, multiple devices, business workflows 2-5 min SMS passes through a third-party service
Open-source APK Personal use, strict privacy, one phone 15-30 min Manual setup and maintenance
Tasker / MacroDroid Power users already using Android automation 30-60 min Fragile rules and battery optimization issues
n8n webhook Teams with existing automation infrastructure 1-2 hours You maintain the workflow and endpoint
Custom app Regulated environments or internal tooling Days/weeks Highest maintenance cost

What SMS-to-Telegram forwarding requires

Every implementation has the same basic pipeline:

  1. An Android app receives incoming SMS using the SMS permissions.
  2. The app formats the message and decides whether it should be forwarded.
  3. A Telegram bot sends the message to a chat, group or channel.

The difference is where the routing logic lives. It can live on the phone, in Tasker, in an n8n workflow, in your own backend or in a SaaS dashboard.

1. Cloud SMS forwarder

A cloud SMS forwarder pairs your Android phone with a web dashboard. Incoming SMS are sent securely to the service, matched against routing rules, then delivered to Telegram, Email, Slack, Discord, X2Chat or a webhook.

Pros: quick QR pairing, multi-device management, routing by sender or keyword, delivery history, team access, webhooks and less manual Telegram bot configuration.

Cons: SMS passes through a cloud service. If your use case requires zero third-party processing, a self-hosted or open-source option is a better fit.

SMS Sender 24 is in this category. It works well when one team needs to share OTP codes, server alerts or business SMS from several Android phones without maintaining Android automation on every device.

2. Open-source Android apps

Projects such as Spirit532 SMS Forwarder and similar APKs can read SMS locally and send them directly to Telegram Bot API or a webhook.

This is the cleanest option if you want to inspect the code and avoid a cloud account.

Pros: free, auditable, no SaaS dependency, good for one phone and one or two destinations.

Cons: you maintain every phone manually. Rules, bot tokens, chat IDs and battery settings are configured per device. Some projects go years without updates, which matters on newer Android versions.

Example Telegram Bot API call:

POST https://api.telegram.org/bot<TOKEN>/sendMessage
Content-Type: application/json
{
  "chat_id": "-1001234567890",
  "text": "SMS from +15551234567: Your verification code is 482913"
}

3. Tasker or MacroDroid

Tasker and MacroDroid can listen for SMS events and call Telegram Bot API through an HTTP action. This is powerful if you already understand Android automation.

Pros: very flexible, cheap, local-first, can combine SMS with other Android triggers.

Cons: hard to support as a team workflow. A missed battery permission, Android update or broken profile can silently stop forwarding. Debugging is usually on the person holding the phone.

This method is great for personal automation. It is less great when three people depend on that phone to receive production alerts or bank OTP codes.

4. n8n webhook workflow

If your team already runs n8n, Make or another automation platform, the Android app can send SMS to a webhook. The workflow can then filter messages and forward them to Telegram.

Example payload:

curl -X POST https://n8n.example.com/webhook/sms 
  -H "Content-Type: application/json" 
  -d '{
    "sender": "AWS",
    "body": "Your alarm is in ALARM state",
    "sim": "work",
    "received_at": "2026-05-23T09:15:00Z"
  }'

Pros: great for teams that already automate alerts and business workflows. You can branch to Telegram, Slack, email or CRM.

Cons: you still need a reliable Android collector app, a public HTTPS endpoint, secrets management, monitoring and retries.

5. Custom Android app

Building your own Android app is reasonable for regulated environments, internal device fleets or very specific routing requirements. It gives you full control over data handling and backend architecture.

Pros: maximum control, private backend, custom security model.

Cons: Android SMS permissions, background execution, foreground services, device reboot handling and battery optimization are easy to underestimate.

The first version is the simple part. Keeping it reliable is the real work.

Android 14/15 pitfalls

SMS forwarding is not just an HTTP problem. Modern Android aggressively limits background work, especially for apps installed outside Google Play.

Watch for these issues:

  • Battery optimization: the app may stop receiving or forwarding after several hours unless excluded from battery restrictions.
  • Foreground service rules: long-running background services need a visible notification and correct service type.
  • Boot handling: forwarding should resume after the phone restarts.
  • Connectivity gaps: the phone needs Wi-Fi or mobile data. Good apps queue messages and retry.
  • Dual SIM: personal and work SIMs often need different routing rules.

Which method should you choose?

Scenario Recommended method Why
One personal phone, one Telegram chat Open-source APK Free and simple enough
Team OTP codes Cloud SMS forwarder Rules, history and team destinations matter
Server SMS alerts Cloud forwarder or n8n Reliability and routing are more important than novelty
Strict privacy / no third party Open-source or custom app Keep data under your control
Existing automation stack n8n webhook Easy to plug into current workflows

Honest note

SMS Sender 24 is an independent cloud tool built to solve a real SMS forwarding problem.

It is convenient for teams, but it is not the right choice if your policy requires zero third-party processing. In that case, use an open-source app or self-host the whole pipeline.

FAQ

Can I forward only specific SMS?

Yes. Use sender names, phone numbers, keywords or SIM slots as routing conditions.

For example, bank OTP messages can go to one Telegram chat while courier updates go to another.

Does the Android phone need internet?

Yes. The phone needs Wi-Fi or mobile data to forward messages.

If connectivity drops, a reliable app should queue messages and retry when the connection returns.

Is this different from SMS gateways like Twilio?

Yes. Twilio and similar services provide virtual numbers or outbound messaging APIs.

SMS forwarding starts from a real Android phone and forwards incoming messages from its SIM card.

Can it work with two SIM cards?

Yes, if the collector app records the SIM slot and the service supports SIM-based rules.

This is useful when one phone has both personal and work SIM cards.

Is Telegram the only destination?

No. Telegram is the most common destination, but many teams also forward to Email, Slack, Discord, X2Chat or a webhook.

Originally published at https://smssender24.com/en/blog/forward-android-sms-to-telegram.html

From AI Code Generation to AI System Investigation

I Released AI DB Investigator – A Skill for Structured Database Analysis

AI is very good at generating answers.

But when it comes to database problems, raw answers are not enough.

A slow query, a missing index, a suspicious schema, a broken relation, or an unclear data model usually cannot be solved by guessing. These problems require investigation.

That is why I released AI DB Investigator.

GitHub repo:
https://github.com/miller-28/skill-ai-db-investigator

What is AI DB Investigator?

AI DB Investigator is a skill designed to help AI analyze databases in a more structured way.

Instead of asking an AI model a broad question like:

Why is my database slow?

The goal is to guide the model into a more disciplined investigation flow.

A good database investigation usually requires several steps:

  1. Understand the schema.
  2. Identify important tables and relationships.
  3. Look at indexes.
  4. Inspect query patterns.
  5. Detect possible bottlenecks.
  6. Separate symptoms from root causes.
  7. Suggest safe, practical improvements.

AI DB Investigator is built around that idea.

Not magic.
Not guessing.
A structured investigation process.

Why I built it

I have spent many years working with production systems, especially around backend architecture, PostgreSQL, Redis, APIs, and distributed flows.

One thing becomes clear after enough production experience:

Database problems are rarely isolated.

A performance issue may look like a slow query, but the real cause may be:

  • a missing composite index
  • a bad join pattern
  • an overloaded table
  • unnecessary eager loading
  • poor pagination
  • wrong data modeling
  • missing constraints
  • too much logic happening in the wrong layer
  • data growth that changed the original assumptions

The database does not fail loudly at first.

It whispers.

Then it slows down.

Then it becomes the center of the fire.

So I wanted a skill that helps AI behave less like a random answer generator and more like a careful technical investigator.

The idea behind the skill

The core idea is simple:

AI should not only answer database questions.
It should investigate them.

That means the AI should slow down and ask the right technical questions before jumping to conclusions.

For example:

  • What database engine is being used?
  • What is the schema?
  • Which tables are involved?
  • Is this about performance, correctness, design, migrations, or reliability?
  • Are there indexes?
  • Are there foreign keys?
  • Is the query using OFFSET pagination?
  • Are there N+1 query patterns?
  • Is the data volume small, medium, or already production-scale?
  • Is the issue caused by the database, the application, or both?

This is the difference between generic AI assistance and operational AI assistance.

What this skill is useful for

AI DB Investigator can help with:

  • database schema review
  • PostgreSQL investigation
  • query analysis
  • index suggestions
  • performance bottleneck detection
  • database design review
  • migration reasoning
  • relationship mapping
  • production debugging
  • explaining suspicious DB behavior
  • turning vague DB problems into structured diagnostic steps

It is especially useful when the problem is not yet clearly defined.

Sometimes the real value is not the final answer.

Sometimes the value is forcing the investigation into the right shape.

Why this matters for AI-assisted development

A lot of AI development today focuses on generation:

  • generate code
  • generate tests
  • generate docs
  • generate APIs
  • generate UI

That is useful.

But production work is not only generation.

Production work is also diagnosis.

A senior engineer spends a lot of time asking:

  • What is really happening here?
  • What changed?
  • Where is the pressure?
  • What assumption is now false?
  • What is safe to change?
  • What should not be touched?
  • What is the smallest useful fix?

That mindset is hard to capture with a generic prompt.

A skill gives the AI a repeatable operating pattern.

That is the direction I find interesting: not only using AI to write code faster, but teaching AI how to approach technical systems with discipline.

Small tool, larger direction

This is not meant to be a huge framework.

It is intentionally focused.

The purpose is to encode a specific kind of engineering judgment into a reusable form.

For me, this is part of a larger shift:

Developers are moving from writing every line manually
to designing, directing, and refining intelligent workflows.

The developer becomes less of a typist and more of an orchestrator.

But orchestration only works when the AI has structure.

Without structure, AI improvises.

With structure, AI can investigate.

Repository

The project is available here:

https://github.com/miller-28/skill-ai-db-investigator

Feedback, ideas, issues, and suggestions are welcome.

Especially around:

  • PostgreSQL investigation flows
  • schema analysis
  • query review patterns
  • useful diagnostic prompts
  • real-world DB debugging examples

Final thought

Database work rewards patience.

The best answers usually come after the right questions.

AI DB Investigator is my attempt to give AI a better path through that process.

Not just to answer.

To investigate.