blog

chronicled escapades in creation

a wristwatch for your agent is really helpful

(this blog post is mostly written by claude fable, based on conversations between me nevyn and it).

An LLM session has no clock. The model experiences the conversation as one continuous now: you ask it something at midnight, close the laptop, come back after lunch, and as far as it can tell no time has passed at all. Whatever it believed about the world at midnight — which branch it was on, that the build was green, that the file it just wrote is still the newest thing in the repo — it still believes, with full confidence, fourteen hours later.

Mostly that’s fine. But I run long-lived Claude Code sessions, sometimes across days, and the same failure kept recurring: the model reasoning happily from a snapshot of a world that stopped being true while I slept. It would say “today” about yesterday. It would keep stacking commits on a branch I’d already merged from another machine.

Nothing in its context suggests that starting server surgery over ssh over a phone hotspot might be a bad idea, because nothing in its context says “phone hotspot” — more on that one in a moment, since it’s the best save the hook has made so far.

So I gave it a watch. And then, since I was in the neighborhood anyway, a little more than a watch.

Two mornings where it earned its keep

First, some examples of where this mechanism saved my bacon; scroll down for the actual scripts and hooks.

This morning I opened a session in my server-config repo that I hadn’t touched in a while. The hook stamped my first prompt with:

<ctx>2026-08-05 07:57 CEST Wed, +400h26m | Alecto | 77% discharging | net hotspot (phone) | raheth@main 2 dirty</ctx>

Four hundred hours. The model’s first words were “Morning! Let me quickly re-verify state — it’s been 2.5 weeks and there are new files in the repo since we last talked” — and the re-verify actually mattered, because in those 2.5 weeks a scheduled recon job had fired and left behind a full upgrade plan for the server that neither of us (in this session’s memory) had ever seen. It found the plan, summarized where things actually stood, and then added, unprompted:

One consideration if you want to do the upgrade now: you’re on a phone hotspot on battery. The upgrade script runs detached on the server so a connection drop mid-run isn’t dangerous, but the backup/verify steps want a stable session — might be nicer from a real connection.

Nothing in my prompt mentioned the hotspot; that came entirely from the 77% discharging | net hotspot (phone) fields. We did the upgrade anyway, but the warning changed how: everything ran inside tmux on a desktop machine at home, so a hotspot blip would cost a re-attach instead of a half-finished backup. Without the stamp, the first sign of trouble would have been the SSH connection dying mid-verify.

The day before, same thing in miniature. A planning session from Tuesday evening, reopened Wednesday morning:

<ctx>2026-08-05 08:03 CEST Wed, +15h31m | Alecto | 73% discharging | net hotspot (phone)</ctx>

First words: “Quick state check so the advice matches reality (big gap since yesterday)”. It re-checked the repos, confirmed that two feature branches had merged since its last look, and only then planned the day — against the world as it was, not as the transcript remembered it. A model without the gap would have given the same advice off yesterday’s state, confidently, and some of it would have been wrong.

The mechanism

Claude Code has hooks — shell commands that run at points in the agent’s lifecycle. The one that matters here is UserPromptSubmit: it runs every time you submit a prompt, gets a bit of JSON on stdin (session id, working directory), and whatever it prints to stdout gets injected into the model’s context alongside your message. That’s the entire trick. The rest of this post is really just about deciding what’s worth printing, which turned out to be the interesting part.

Mine prints one line, and only sometimes:

<ctx>2026-08-05 22:30 CEST Wed | Alecto | 31% charging | net unknown (gw aa:bb:cc:dd:ee:ff)</ctx>

Pipe-separated: timestamp, hostname, and then a handful of fields that only appear when they have something to say — battery, network location, the git state of the directory we’re standing in, and whether my config-sync repo is in a conflicted state.

The gate is most of the point

The script does not print on every prompt. It keeps a tiny state file per session (keyed by session id, in $TMPDIR), and if less than ten minutes have passed since the last stamp, it prints nothing and exits.

Partly this is about tokens, but mostly it’s about habituation. A stamp on every prompt becomes wallpaper; the model tunes it out the way you tune out a ticking clock. Gated, the stamp’s mere appearance is information: time has passed. A fresh session always opens with one, since there’s no previous timestamp to gate against.

And when it fires after a real gap, the gap itself gets printed:

<ctx>2026-08-06 09:12 CEST Thu, +10h42m | Alecto | ...</ctx>

That +10h42m is the most load-bearing part of the whole line. “It is now Thursday morning” is trivia; “ten hours passed since you last looked at anything” is an instruction, or at least it becomes one, because my CLAUDE.md tells the model what a big gap means: re-verify build and branch state before reasoning from memory. The world may have moved. It usually has.

Fields that appear only when they mean something

Battery and network follow the same attention-economy logic. The state file remembers the last power state and network label, and the field is only printed when it changed — with one exception, discharging, which prints every stamp, because a laptop draining is a fact you want standing in the room.

The network label is my favorite dumb trick in the script: there’s no API for “where am I”, but your default gateway’s MAC address is a fingerprint for the network you’re on. route -n get default for the gateway IP, arp -n for its MAC, then a lookup in a little hand-maintained table mapping MACs to labels like “home” or “office”. iPhone hotspots don’t need an entry at all — they hand out a recognizable subnet, so those become hotspot (phone) by prefix match. An unlisted network prints unknown (gw <mac>), which doubles as the enrollment flow: visit somewhere new, the MAC shows up in the stamp, paste it into the table with a label. The point of all this is the combination: discharging plus hotspot (phone) should make an agent reconsider kicking off anything long and network-hungry, and now it can.

Git state, or: did someone push behind my back

The git field packs a surprising amount into a few words:

myrepo@feature/foo [worktree] REBASING 3 dirty, ahead 2, behind 1

Repo and branch from rev-parse; a [worktree] marker by comparing the realpaths of --git-dir and --git-common-dir (they differ exactly when you’re in a linked worktree); rebase/merge/cherry-pick detected the way git itself does it, by marker files in the git dir; dirty count and ahead/behind parsed out of status --porcelain=v1 -b, whose header line carries them for free.

This field earns its place because agents are shockingly confident about repo state they remember rather than observe. I move between machines. I push things mid-session from another terminal. Worktrees are their own special footgun — a session that doesn’t know it’s standing in one will happily reason about “the” checkout. Having the actual state ambient in context, refreshed every stamp, catches a whole family of stale-assumption bugs before they start.

The legend lives in the system prompt

The stamp can afford to be terse because it isn’t self-describing — the decoder ring lives in CLAUDE.md, next to a sentence that turned out to be essential: these blocks are ambient, no need to address them unless relevant. Without that line, the model comments on your battery percentage like a nervous flight attendant. With it, the information just sits there, shaping decisions when it matters and ignored when it doesn’t, which is more or less what ambient means.

Fail soft

Every probe is wrapped in a try/except that returns None; subprocesses get a 1.5 second timeout; anything missing just shrinks the line. Desktops have no battery, offline machines have no gateway, most directories aren’t repos, and none of that should matter — a hook that can block or garble prompt submission is much worse than a missing stamp.

Keeping two machines honest

The stamp says Alecto because there are two of these machines — Alecto the laptop, miquon the desktop — and the whole arrangement only works if the hook, the CLAUDE.md legend that decodes it, and the rest of the config are identical on both. That’s its own little sync system, and it dovetails with the hook in a way I didn’t plan but quite like.

The config lives in a git repo (~/Dev/ClaudeSynced); ~/.claude reaches it through symlinks that an install script sets up on a new machine. A SessionEnd hook — the bookend to UserPromptSubmit — commits everything as sync (hostname), does pull --rebase --autostash, and pushes. Every step is best-effort with output appended to a log, because a sync hook that can wedge session teardown is the same bad trade as a stamp hook that can wedge prompt submission.

Two wrinkles were non-obvious. Claude Code rewrites its own settings via write-temp-then-rename, which quietly replaces the symlink with a regular file — so the sync hook checks each config path, and where it finds a real file it folds the content back into the repo and re-links. And when the rebase hits a real conflict, nothing automatic is safe anymore; the repo just sits there mid-rebase. Which is where the last field of the stamp comes in: sync_line() checks for rebase/merge markers in the sync repo, and prints CONFIG-SYNC CONFLICT if it finds them. The model itself becomes the alarm bell, mentioning it every stamp until I fix it — which is, more or less, the only notification channel I actually read.

The script

Registered in ~/.claude/settings.json:

"hooks": {
  "UserPromptSubmit": [
    {
      "matcher": "",
      "hooks": [
        { "type": "command", "command": "$HOME/.claude/hooks/timestamp-context.py" }
      ]
    }
  ]
}

And the script itself, in its entirety (chmod +x it):

#!/usr/bin/env python3
# UserPromptSubmit: when >10min elapsed since last stamp (per session), inject
# one pipe-separated <ctx> line: timestamp (+gap), host, battery and network
# (only on change, or while discharging), git state of cwd (worktree, rebase,
# ahead/behind), config-sync health. Legend lives in CLAUDE.md (Working rules).
import sys, json, os, re, time, socket, subprocess
from datetime import datetime

GATE = 600  # seconds

# Gateway MAC -> location label. At an unlisted site the stamp shows
# "unknown (gw <mac>)"; paste that MAC here with a label. iPhone hotspots
# are detected by subnet, no entry needed.
GATEWAYS = {
    # "aa:bb:cc:dd:ee:ff": "home",
}

def run(*argv, timeout=1.5):
    return subprocess.run(argv, capture_output=True, text=True,
                          timeout=timeout).stdout

def battery():  # -> (pct, state) or None on desktops
    try:
        m = re.search(r"(\d+)%; ([^;]+)", run("pmset", "-g", "batt"))
        return (m.group(1), m.group(2).strip()) if m else None
    except Exception:
        return None

def net_label():
    try:
        gw = re.search(r"gateway: ([\d.]+)", run("route", "-n", "get", "default"))
        if not gw:
            return None
        if gw.group(1).startswith(("172.20.10.", "192.168.43.")):
            return "hotspot (phone)"
        mac = re.search(r"at ([0-9a-f:]+)", run("arp", "-n", gw.group(1)))
        if not mac:
            return None
        return GATEWAYS.get(mac.group(1), f"unknown (gw {mac.group(1)})")
    except Exception:
        return None

def git_line(cwd):
    try:
        info = run("git", "-C", cwd, "rev-parse", "--path-format=absolute",
                   "--show-toplevel", "--git-dir", "--git-common-dir").splitlines()
        if len(info) < 3:
            return None
        top, gitdir, common = info
        head, *entries = run("git", "-C", cwd, "status", "--porcelain=v1",
                             "-b").splitlines()
        branch = head[3:].split("...")[0]
        if branch.startswith("HEAD"):
            branch = "detached"
        name = f"{os.path.basename(top)}@{branch}"
        if os.path.realpath(gitdir) != os.path.realpath(common):
            name += " [worktree]"
        for marker, flag in (("rebase-merge", "REBASING"),
                             ("rebase-apply", "REBASING"),
                             ("MERGE_HEAD", "MERGING"),
                             ("CHERRY_PICK_HEAD", "CHERRY-PICKING")):
            if os.path.exists(os.path.join(gitdir, marker)):
                name += " " + flag
                break
        parts = [f"{len(entries)} dirty" if entries else "clean"]
        if "[" in head:  # "## main...origin/main [ahead 1, behind 2]"
            parts.append(head.rsplit("[", 1)[1].rstrip("]"))
        return f"{name} {', '.join(parts)}"
    except Exception:
        return None

def sync_line():
    g = os.path.expanduser("~/Dev/ClaudeSynced/.git")
    if any(os.path.exists(os.path.join(g, p))
           for p in ("rebase-merge", "rebase-apply", "MERGE_HEAD")):
        return "CONFIG-SYNC CONFLICT in ~/Dev/ClaudeSynced (automemory-sync.log)"
    return None

data = json.load(sys.stdin)
sid = data.get("session_id", "default")
state = os.path.join(os.environ.get("TMPDIR", "/tmp"), f"cc-ts-{sid}")

now = time.time()
prev = {}
try:
    raw = open(state).read().strip()
    prev = json.loads(raw)
    if not isinstance(prev, dict):
        prev = {"t": float(prev)}  # pre-JSON state files held a bare float
except (FileNotFoundError, ValueError):
    pass
last = prev.get("t")

if last is not None and now - last < GATE:
    sys.exit(0)  # within gate: inject nothing

ts = datetime.now().astimezone()
stamp = ts.strftime("%Y-%m-%d %H:%M %Z %a")
if last is not None:
    gap = int(now - last)
    stamp += f", +{gap // 3600}h{(gap % 3600) // 60:02d}m"
fields = [stamp, socket.gethostname().split(".")[0]]

batt = battery()
if batt and (batt[1] != prev.get("power") or "discharg" in batt[1]):
    fields.append(f"{batt[0]}% {batt[1]}")
net = net_label()
if net and net != prev.get("net"):
    fields.append(f"net {net}")
for f in (git_line(data.get("cwd") or os.getcwd()), sync_line()):
    if f:
        fields.append(f)

with open(state, "w") as f:
    json.dump({"t": now, "power": batt[1] if batt else None, "net": net}, f)

print(f"<ctx>{' | '.join(fields)}</ctx>")

(The sync_line bit checks whether the git repo holding my synced config is stuck mid-merge, which is specific to my setup — rip it out, or repoint it at whatever repo you’d want to be warned about.)

The macOS-isms — pmset, route/arp output formats — would need swapping on Linux, but each probe is a small function returning a string or None, so the surgery is contained.

I keep being tempted to add more fields. Calendar awareness feels like it belongs here, maybe CI status. But every field has to pay rent in attention, and so far these are the ones that have.

My agentic coding workflow in March, 2026

After years as an AI skeptic, it’s my turn to admit I was wrong. Just half a year ago, LLMs would waste more of my time than they would save me. Today, I can write an entire SwiftUI app with zero human intervention at the code level (and I do). So for me, that means it’s time to embrace this as the future of coding, learn the tools, and get empowered instead of complaining.

What level of LLM written code versus manual written code is correct for a stable code base, or the ethicality of using AI in development… these are not the questions I’m here to answer today. This blog post is about what I’ve learned in the past few weeks, and the workflow that I’ve set up to maximize my productivity, both at work and for hobby projects.

AI coding moves at the speed of light, but much of the below should still be solid advice for quite a while. One thing I really enjoy about agentic coding is that gives us one more reason to do pedantically good and correct coding: we’re forced to write documentation, to comment our code, to write unit tests, and to automate processes, just to reap the real benefits of AI. This is great for people and agents alike!

Workflow setup

Screenshot of my agentic workflow with Xcode, VSCode and Claude VSCode extension

My general workflow (as seen in the screenshot above) is Visual Studio Code with the Claude extension, and a Claude Max subscription, and Xcode on the side if needed. For my dayjob, I use Xcode with Codex on the side.

For projects that are mostly vibe coded (i.e., I let Claude do almost all the coding, and I only code if I really have to), I like having the agent conversation front and center as the main tab, and context for the agent on the right. In Vscode, you get this by clicking the orange * icon in the toolbar, instead of using the agent sidebar.

I vastly prefer to have a GUI tool for my agents. I don’t quite understand why people run agents as CLI TUIs instead of dedicated apps or vscode extensions. In a GUI app, plan files render with proper markdown headings, inline images, graphs; and it’s easier to tell chat messages from code diffs, and navigating between agent sessions, and… Yeah. If you’re undecided, I strongly recommend a GUI tool like a VSCode extension, or Cursor or similar.

To help agents with context for their work, I maintain README.md, AGENTS.md, BACKLOG.md and MEMORY.md in each of my projects. Read more about them below.

A typical session for a vibe coded session

Before I dive into specifics, I’ll just run you through a typical session, to give you a feel for how I work. Then I’ll explain my rationale for each. So, when I decide to go from an idea to a project…

First, I’ll create a new Git repo. I do nothing without Git. Just mkdir myproject; cd myproject; git init; code .. Even for people with no coding experience, I recommend learning the basics of git and basing their work on top of that.

Next, I’ll describe the vision for the project as much as I can in a README.md. I could put this as the first message in a chat session, but then I’d have to repeat it in the next session. Much better to have it in the repo.

If my vision is too grand, I’ll add a docs folder and add a bunch of Markdown files and screenshots and drawings to explain it in as much detail as possible.

I’ll then draft an AGENTS.md with my rules for how to work together effectively. This includes atomic commits, continuously working with a MEMORY.md, and building and testing.

I’ve probably already configured whatever MCP and tool access is needed to enable it to then follow those rules.

I’ll probably fire up a chat session next, asking it to first critique my plan; and then ask it to establish the skeleton for this project. In this session, we’ll probably flesh out a few first basic features.

After a while, I’ll have too many todo’s in my head, things I want to have it build. So I’ll add a BACKLOG.md, as a very lightweight tool for keeping track of upcoming tasks.

Then I can prompt it to keep building features from backlog, stopping every few features to test them out and keep it honest and check stability; or interject with specific todo’s that I’m more keen on in the moment.

For anything that is big, I’ll either ask it to make a plan, or it will make a plan from its own volition. This is your time to shine, and really check for any holes in its idea of how to implement things, or for context or future ideas that you have in your head and never properly communicated. Auto-accepting plans without reading through and directing is the surest way to end up with an unreadable spaghetti code base.

After a while, the project becomes beta release ready, and I’ll have it configure CI and a release process, so we can get server deployments and/or binary downloads ready for the user.

I’ll probably also make a marketing website and get that published on Github Pages.

And iterate! From here, we can go forever, with however many features and improvements as we want.

Main learnings

Okay, half of that might’ve gone over your head, unless you’re already deep in the agentic coding space. Let’s dive into the practicalities of this workflow.

I used to just write in the ChatGPT app and copy-paste back into Xcode. I’ve come a long way since. In that journey, some fundamentals I’ve established are outlined below.

Provide the agent with ways to test its own work.

Avoid roundtrips to you as a user. Any time you have to answer “is this correct/done?”, you’ve wasted time on back-and-forths that could’ve been spent better elsewhere.

Enable the agent compile on its own so it can fix all compile errors. In a strongly typed language like Swift, in a good code base, code that compiles is generally correct code. This will save you SO much time.

(Note that this implies: don’t use weakly typed languages with LLMs. The agent has no way to validate the correctness of its code without running it and noticing the runtime errors. For some code paths, that’s almost impossible without a real life usage scenario, so your code will start to get riddled with unchecked bugs.)

Enable the agent to generate screenshots and look at the visual results of its coding so it can fix things without your intervention. Instead of doing five roundtrips of you sending it screenshots, it can give itself the feedback it needs.

For Xcode, this means: give it access to xcodebuild, or build through Xcode MCP. Xcode MCP also gives it access to #Preview, so it can look at the result of its code.

You can give Claude access to your Xcode with claude mcp add --transport stdio xcode -- xcrun mcpbridge.

For other environments, you can ask it to implement snapshotting/screenshotting, so that it can launch your app/website/whatever and check its work.

Session memory, managing context length, and AGENTS.md

Sessions have rather limited memory, often around 250k tokens (about a million characters). A long conversation, and reading a lot of your local source code, will exhaust this space. Claude can then compact your conversation (which means, write a summary of everything, and use this summary instead of the whole conversation as input to the next message); or you can create a new session.

In both cases, it will forget important details. It can then either do the research again, or you can help seed it with important context.

AGENTS.md (or with Claude, CLAUDE.md) is just a text file with rules and context information. Some use it as a tool mostly to describe the structure of your code: I think this is wrong. Put only the most important architecture rules in AGENTS.md, and put the rest in real documentation files (in docs/, or alongside code). Architecture and design information applies equally to agents and humans, so write it for both.

Instead, use AGENTS to describe rules and workflows. Here’s what a typical AGENTS.md looks like for me:

## RULES (always follow, never skip)
- All errors must be caught and forwarded so they can be surfaced in the UI.
- Always make atomic commits with detailed commit messages after each completed subtask. Do staging and committing as separate tasks so I don't have to approve the commands.
- Check off todo items in BACKLOG.md and put them into Completed at the bottom of the document. Include this change in the commit.
- Write unit tests when it makes sense. Especially when you fix a bug, see if you can write a unit test to make sure it doesn't happen again.
- Make #Previews for all the important views. Use these through MCP when making views, both previewing mac and iPhone, so you know if it will look good.
- Always build with xcodebuild to make sure things compile. Use Xcode MCP when available and needed (to run, make snapshots, look at SwiftUI previews, etc). Always verify that the app builds for iOS and MacOS, and at the end of each plan, also run all unit tests with xcodebuild.
- Keep a Memory.md as a memory for you between sessions. Keep it under 100 lines. Focus on things you can't easily rediscover from code: architecture overview, build commands, non-obvious pitfalls/gotchas (where you'd waste time if you forgot), test infrastructure quirks, and user preferences. Don't document settled features in detail (library system, navigation, cover art, etc.) — you can read the code for those. Update with each commit.
- At the start of every new task, re-read claude.md and memory.md.

More details follow, but keep the above always in memory with highest priority.

## Git Workflow

* **Atomic commits per logical change.** Each distinct feature, fix, or refactor gets its own commit — do not bundle unrelated changes together.
* **Detailed commit messages.** The subject line names the module in parentheses and describes *what* changed. The body explains *why* and *how*: what the key design decisions were, what invariants are maintained, and what other files were deliberately left unchanged.
* **Check off completed backlog items.** When a commit completes a BACKLOG.md item, include the checkbox update (`- [x]`) in the same commit, and move the item to the top of the "# Completed" section"


## Error Handling: Fail Fast and Early

...

## Architecture Boundaries

...

## Testing

...  and so on. you get the point.

Let’s call out the most important bits here:

  • At the top of AGENTS.md, I try to succinctly keep the most important rules in one place. Even if this whole file is long, LLMs will prioritize information in the beginning, especially if you tell it something is important.
  • I’ve asked Claude to keep a MEMORY.md. This lets it record its most important discoveries in a place it can re-reference. I feel like this improves the time it takes the agent to re-acquire context, but I have no data on that.
    • I’ve heard other people use things like: an sqlite database! to store an infinite amount of learnings. Or, store a LOT more info in markdown files and use something like qmd to query this massive information store. I haven’t tried either of these approaches.
  • Atomic commits. This is huge, and I’ll expand on this below.
  • Unit testing. I have specific preferences for how to unit test, so I ask it to work like that to make sure that the agent doesn’t break functionality as it adds more features.
  • Other code base hygiene factors. If you are using open source dependencies, some require you to mention them and their license from within your app. This is the kind of rule that goes perfectly in AGENTS, to make sure your agent remembers to add new licenses to this list.
  • This is also a good place to document un-obvious things. Some design decisions might be counter-intuitive, and rather than have every agent session think it’s a bug, document it in AGENTS.

Atomic commits

I love Git. It can be a menace, but it is an incredibly helpful tool to work with changes to code.

By asking Claude to always make commits as it goes, as atomic, fully functional commits, I can always just roll back a commit if I disagree with the direction. Or with almost minimal effort revert a commit from history if I changed my mind.

By asking Claude to always write almost over-verbose commit messages, explaining the rationale behind the change, I can always git blame a line to know exactly why that line exists, and why it wasn’t written another way. And importantly, future agents can know this too.

A screenshot of long commit messages.

I prefer both of these rules when I work with humans too, but agents are much better at adhering to them.

I ran into this benefit just a few minutes ago. I ran into a code signing issue with a Mac app — a well known finnicky concept that often breaks, and it’s hard to remember exactly the steps one took to fix it last time, as you likely tested a dozen different solutions. I could just ask the agent to look at git history, and not only could it find WHAT it should do to fix it again, it could understand WHY, and tell me!

Working with a backlog: you are now a manager.

If you’ve ever project lead something, you know how important it is to have a roadmap, and vision for where you’re going. This is true for your vibe coded project too. Just mashing out whatever feature comes to mind is fine up until a point, but eventually you’ll run into your project being a tangled mess — and I’m not just talking about code quality, but product quality. You need to have a vision and a road there to make something good.

We COULD set up a JIRA or other project management tool, but… Since everything else is a markdown file in this agentic world, why not the backlog too?

A screenshot of a backlog.md, with categories bugs, smaller things, bigger things, refactors needed, etc, and Completed.

I really like having the “Completed” section. That way, we have a log of all our requests, which can give us perspective on what we’ve built, and also give agents more context.

But, I have also had success integrating Claude with Notion, through MCP! MCP is a standardized protocol for connecting agents to tools, both local and remote. By letting my agent talk to Notion, where I have my backlog, I can have it pick up tasks, read more from product design specifications, sketches and what else I have up there, write the code, make a PR and attach it to the card, and move the card to “in review”, all without my interaction.

A screenshot of a backlog in Notion

A screenshot of a conversation where Claude asks me which card it should pick up next.

Regardless, take this to heart: if you are to vibe code successfully, you are practically becoming an engineering manager. You steer direction from a vision, and you course correct the agent if it ever veers off that path, or goes down a bad coding decision that will lead to a nightmare code base further down. This is where your years of programming experience can really show its use in this age of agentic coding.

Working with CLI tools

For some services, I had real trouble setting up MCP. It would be buggy, hard to authenticate, or just wouldn’t work.

Then I realized, Claude can just use command line tools, and learn to use these tools by reading their documentation. So instead of adding MCP, I could just install and configure tools for pretty much anything! And it works so great.

  • I use gh to have Claude submit pull requests, create releases, rename projects, configure Github Pages, you name it.
  • I use glab to have Codex do the same things with GitLab, including making Merge Requests, adding appropriate colleagues to review the feature, and summarizing my changes
  • I use asc to have Claude download crash reports from App Store Connect, enable TestFlight releases, submit translated metadata for appstore listings, etc

Have scripts invoke Claude

One really neat hack is a release.sh script I had Claude code for me, which creates a new GitHub release, which triggers CI to build a release of my app, and then uploads the app’s zip to said release.

This script also writes the changelog and attaches it to the release using claude -p! So Claude wrote this script that uses Claude to write release notes. Meta! It took a while to tweak the prompt, so here it is for your perusal:

if command -v claude &>/dev/null && [[ -n "$LAST_TAG" ]]; then
    echo "Generating release notes with Claude (changes since ${LAST_TAG})…"
    echo ""

    # Feed Claude both the commit log and the Swift diff so it has full context
    # even when commit messages are terse.
    COMMIT_LOG="$(git log "${LAST_TAG}..HEAD" --format='%s%n%b' -- 2>/dev/null || true)"
    SWIFT_DIFF="$(git diff "${LAST_TAG}..HEAD" -- '*.swift' 2>/dev/null || true)"

    NOTES="$(cat <<EOF | claude -p --output-format text 2>/dev/null || true
You are writing release notes for Melur, a music player app for macOS (and iOS).
Below are the git commits and Swift code changes since the last release (v${CURRENT_VERSION}).
Write a short, friendly release notes body (2–5 bullet points) describing user-facing changes.
Omit version-bump commits and internal/CI plumbing. Use plain markdown bullet points, no header.
You are running non-interactive, so go ahead and decide on your own if you become indecisive
about anything.

Your response is directly piped to the gh tool, so IT IS VERY IMPORTANT that you do not say
any commentary, notes or disclaimers. ONLY say the bullet points for the release notes in
response to this prompt.

## Commits
${COMMIT_LOG}

## Swift diff
${SWIFT_DIFF}
EOF
)"

Claude permissions

One of my agents.md rules is, “do staging and committing as separate tasks so I don’t have to approve the commands”. This is because when a command contains ”&&”, Claude just assumes that it is too tricky to whitelist in settings.json and prompts the user to approve it.

A session can be tens or hundreds of cli commands, and anytime you have to babysit prompts that aren’t actually dangerous, you’re wasting time and attention. Use AGENTS.md prompts like the above to make it easier to whitelist commands to trust, and then curate your .claude/settings.json thoroughly to make it safe AND productive. Here’s a snippet from one of my configs:

{
  "permissions": {
    "allow": [
      "Bash(find *)",
      "Bash(swiftc *)",
      "Bash(git -C /Users/nevyn/Dev/melurian *)",
      "WebFetch(domain:github.com)",
      "WebFetch(domain:raw.githubusercontent.com)",
      "Bash(xcodebuild *)",
      "Bash(xcrun swift *)",
      "Bash(curl *)",
      "WebFetch",
      "WebSearch",
      "mcp__xcode__XcodeListWindows",
      "mcp__xcode__BuildProject",
      "mcp__xcode__XcodeLS",
      "mcp__xcode__XcodeRead",
      "mcp__xcode__XcodeWrite",
      "Bash(gh run *)",
    ],
  }
}

When the agent fails

Even if you carefully manage PLAN files, fill out backlogs, and write architecture specs, sometimes the agent will fail to perform your task, or just do a bad job of it.

My general approach is: if the agent gets stuck for more than 2-3 iterations, take the wheel. You need to be able to step in and fix it, and no matter what you prompt, it will just make the mess worse. You have reached the edge of its capability, and even if it feels like it’s almost getting it, you won’t get there.

This is also where those atomic commits pay off — just revert or git reset --hard HEAD~3 to remove the last few commits and try a different approach, or a fresh session.

How much should you review code, to notice that things go wrong? That depends on the importance of the result. If you’re building a hobby project for the fun of it, mostly only reviewing plans carefully and never reading the code actually works fine, up to tens of thousands of lines of code.

For work where I get paid for writing software, I audit every line and make sure I understand what’s going on. If I don’t, I ask the agent to reiterate until I’m both pleased with the quality, and understand exactly what’s going on. Some say this is overkill; I say it is a necessity to not lose control of your software stack.

Future agentic coding wish list

My biggest wishlist is to be able to have my agents continue working when I put my laptop to sleep. Preferably, agents would auto-migrate between my computer and cloud containers as needed, and never be entirely bound to my machine. Compilation tool access, and Github automation, and a few other MCP capable tools, should be able to run just as fine in a docker container in a cloud instance as on my machine. And I’d love to be able to choose freely and jump back and forth between directing my agent from vscode, the browser, or a mobile app on the go, regardless of how I started the session.

Claude Remote has the very beginnings of this, but it’s very rough and CLI only at the moment.

I also know some people use multiple collaborating agents to fill many different roles. I’d love to learn more about this, but haven’t had time to research it yet.

Thank you

If you’ve read this far, thank you for following along! Please reach out to me at my socials or hello@nevyn.dev if you have thoughts or questions.

This blog post was written 99.9% by hand, and proofread by Claude.


Addendum, 2026-03-10

There is so much more to say on this topic. Things that have come to mind after posting are:

  • Git worktrees are great. You do need to have a solid grasp of Git first, though, but the general idea is: Create one more working copy of the current repo, but use the same .git folder for it. This’ll save you on storage and processing, and more importantly, you can very easily merge branches between working trees without even pushing stuff. I put .worktrees in my user .gitignore, so that I can ask my agent to create a worktree for each parallell agent I have running. I would never let multiple agents loose on a single working copy. Use work trees! Codex even has them built in!
  • I’m fairly new at agentic coding. If you want more perspectives, I have really enjoyed:

Hello, bliki

I’ve read and admired the Tao of Mac for over a decade, and long wanted to make my own combined blog and wiki. And, somewhere for my projects to have a combined home, so people can know about them!

Unfortunately, I’m not a web dev. Fortunately, in this era of agentic coding, I don’t have to be!

The idea is to have a single space for my meanderings (blog), reference pages (wiki) and portfolio bits, all in one place.

Thanks for reading, and hope to hear from you :)