sara. PT

Writing

Twenty junior devs and a suggestion they did not have to understand

· agent-harness-cookbook· Claude Code· hooks· onboarding

regra-com-mecanismo picked a rule that’s easy to state cleanly: never assign a field directly, always go through the service. Most rules a junior dev actually needs aren’t that clean. The failure isn’t usually “did something destructive happen” — it’s quieter than that. It’s a suggestion accepted because it compiled, not because anyone understood why it was right. CLAUDE.md can say “read carefully before accepting a suggestion.” It cannot make that true, for the same reason it can’t make any rule true: it’s a sentence in a context window, not something that happens at the moment of the decision.

onboarding-junior is the sixth example in the cookbook, and it’s the first one in the series that doesn’t try to detect a bad change. It tries to force one specific, cheap thing to exist before a risky change lands at all: a sentence saying why.

What counts as “sensitive” here

export const SENSITIVE_PATTERNS = [
  { id: "OJ01", pattern: /^app\/Services\//, area: "a service class" },
  { id: "OJ02", pattern: /^database\/migrations\//, area: "a migration" },
];

Business logic and schema — the two categories where “it worked when I ran it” and “it’s correct” are furthest apart, and where a junior dev is least equipped to tell the difference on sight. Everything else — a view, a stylesheet, a config file — passes through untouched. The hook isn’t trying to slow down all writes; it’s trying to slow down exactly the ones where understanding matters most and is least likely to already be there.

Reading the transcript instead of trusting a flag

The obvious lazy version of this hook takes a justification field straight out of tool_input and checks its length. That version proves nothing — it only checks that something was passed in, and whatever calls the hook controls what that something is. The actual mechanism reads the assistant’s own preceding message out of the conversation:

export function lastAssistantText(transcriptText) {
  const lines = transcriptText.trim().split("\n");

  for (let i = lines.length - 1; i >= 0; i--) {
    let entry;
    try {
      entry = JSON.parse(lines[i]);
    } catch {
      continue;
    }

    const role = entry?.type ?? entry?.message?.role;
    if (role !== "assistant") continue;

    const content = entry.message?.content ?? entry.content;
    if (Array.isArray(content)) {
      return content.filter((b) => b?.type === "text").map((b) => b.text).join("\n");
    }
    if (typeof content === "string") return content;
  }

  return "";
}

Claude Code already hands every PreToolUse hook a transcript_path — the JSONL file of the session so far. This reads the last assistant turn out of it and looks for a Justification: line in what the assistant actually said, before proposing the write, not in a field a caller decided to attach. The distinction matters the same way it mattered in gate-shell: the hook has to observe something that happened, not accept a claim about what happened.

The length check is honest about what it can’t do

if (justification.length >= MIN_JUSTIFICATION_LENGTH) {
  return { decision: "allow" };
}

Twenty characters. That’s not a quality bar — “fixes it, trust me” is nineteen characters short of qualifying only by accident, and twenty characters of padding would sail through exactly as well as a real reason. The mechanism isn’t grading the reasoning. It’s forcing the act of writing one down, on the theory that the sentence itself is the intervention: a dev who has to type why a change is correct, even badly, has to form an opinion about it first. A dev who never has to write anything can accept forever without ever forming one. Whether the sentence is right is still a job for a senior reviewer reading the diff — this only guarantees there’s something for them to read.

What transfers

The two path patterns are Acme Invoices’ businesses-logic and schema directories; a real team names its own. What transfers is the shape of the intervention: pick the category of change where “compiled” and “correct” diverge the most, and force a one-line commitment to exist before that category of change lands — read from something the agent actually said, not from a value a caller can set to whatever makes the check pass.

Limitations

  • The check doesn’t validate content, only presence. A junior dev who learns to type filler text defeats the mechanism while still satisfying it — this is a floor, not a review.
  • Two path patterns, illustrative. A real rollout needs the actual sensitive paths for its own codebase, and there are usually more than two.
  • Only the immediately preceding assistant turn is read. A justification written three turns earlier and still relevant to the current write is invisible to this check.
  • This is a floor, not a review process. It guarantees a sentence exists before a risky write lands. It does not guarantee a senior dev ever reads it.