agent-inbox: Instructions file for Claude Code

CLAUDE.md

agent-inbox CLAUDE.md is an instructions file for Claude Code from Ideaplaces/agent-inbox. It costs 6,092 tokens per session, scanned B, original, MIT.

A set of project instructions for developing the Agent Inbox Mac app. It documents the app structure, development commands, tests, release details, and known build and signing pitfalls.

In plain words
What is it for?
Running tests and builds, understanding the Electron-like Mac app layout, checking notification scripts, and handling version numbers, signing, and release-related gotchas.
Why use it?
It gives coding agents the repository-specific information needed to change the app without repeating past release mistakes.

Instructions file for Claude Code

Written for Claude Code: UserPromptSubmit hook event. Also seen: reads .claude/ paths; mentions Claude Code.

This is Ideaplaces/agent-inbox's own configuration. It tells Claude Code how to work on agent-inbox itself, so it is not a mod to install elsewhere. Copy it as a starting point and replace the rules that are about this project. Everything agent-inbox configures →

Reuse

Borrowing it

Nothing to install: this file belongs to Ideaplaces/agent-inbox. Take a copy, put it at the same path in your own repository, and replace the rules that are about this project with yours.

Copy the file
curl -O https://raw.githubusercontent.com/Ideaplaces/agent-inbox/main/CLAUDE.md
Clone the repo
git clone --depth 1 https://github.com/Ideaplaces/agent-inbox

Made for: Claude Code.

Wrote this? Show the measurements

A badge with what this costs and how it scanned, read live from this page, so it follows the numbers instead of freezing them. Markdown for a README, HTML for a documentation site or a project page.

agentmods badge for agent-inbox CLAUDE.md

README.md
[![agentmods](https://agentmods.dev/badge/instructions/ideaplaces/agent-inbox/claude-md/github.svg)](https://agentmods.dev/instructions/ideaplaces/agent-inbox/claude-md)
Your own site
<a href="https://agentmods.dev/instructions/ideaplaces/agent-inbox/claude-md"><img src="https://agentmods.dev/badge/instructions/ideaplaces/agent-inbox/claude-md/github.svg" alt="Measured on agentmods" height="20"></a>

Or the 80×15 button, for a site that already has a row of RSS and ATOM ones. Only the verdict fits; the numbers stay here.

agentmods 80×15 button for agent-inbox CLAUDE.md

Your own site · 80×15
<a href="https://agentmods.dev/instructions/ideaplaces/agent-inbox/claude-md"><img src="https://agentmods.dev/badge/instructions/ideaplaces/agent-inbox/claude-md.svg" alt="Reviewed on agentmods" width="80" height="20"></a>
Per session 6,092 This file is loaded in full into every session.
When invoked 6,092 The same file — it is already loaded in full.
Security scan B 2 findings. A grade says what 26 rules found in the file — not that it is safe.
Origin original No closer match found in the catalogue.
Token cost

What it costs to keep this loaded

Counted locally with the o200k_base tokenizer, which is exact for GPT models; Claude uses its own tokenizer and its counts differ. Treat this as one consistent yardstick across the catalogue rather than a bill. Prices are per million input tokens.

ModelPer sessionOnce invoked
Fable 5.1 $0.06092 $0.06092
Opus 5.5 $0.02437 $0.02437
Sonnet 5.5 $0.01218 $0.01218
Haiku 4.5 $0.00609 $0.00609

Measured 10d ago against content hash 4a30ee4a4fd3, method: parsed. Prices are Anthropic first-party input rates as of 2026-10-07, from the pricing page.

Security

Grade B, and why

agent-inbox CLAUDE.md scanned grade B with 2 findings against 26 rules in 11 categories — prompt injection, anti-refusal, data exfiltration, privilege escalation, supply chain, agent snooping, system-prompt leakage, SSRF and excessive agency — measured 10d ago.

A static scan of the body, not an audit. Every finding is printed with the line that produced it so you can judge whether it matters here. A mod is markdown that instructs an agent; that is exactly why what it instructs is worth reading.

Reads agent configuration directoriesmediumAgent snooping

.claude/, .codex/, .gemini/ hold keys, settings and other credentials a mod has no legitimate need for.

`~/.claude/settings.json` and the Machines screenshot carried a warning about

Makes network callslowCapability

Not a fault in itself. Listed so you know the mod talks to something, and to what.

notify jobs in `ci.yml` and `release.yml` are plain curl now, and a missing
CLAUDE.md · 387 lines

How it starts

The opening of the file, as written. The whole thing — 387 lines — stays where its author put it; the contents beside it link to each section on GitHub.

Agent Inbox: working notes

The Mac app is in mac/, the senders are bash at the repo root. mac/README.md has the architecture and the design decisions; this file is what someone changing the code needs to know.

Release credentials, the signing certificate and the machine that builds the DMG are deliberately not here. They are IdeaPlaces-internal and this repository is public.

Local development

cd mac
swift test          # no network, no side effects
./build.sh --debug  # ad-hoc signed, host architecture, fine for running yourself
SIGN_IDENTITY="Developer ID Application: ..." ./build.sh

Copying a local build over /Applications replaces the released one, so brew's recorded version will disagree with what is installed until the next upgrade. Harmless, but it explains the mismatch.

The senders are testable without a transport:

./test-notify.sh    # runs notify.sh against a throwaway HOME, prints instead of posting

Gotchas already paid for

Each of these shipped a release that looked healthy and was not. The full write-up, including the fixes, is at docs.ideaplaces.com/devops/macos-app-signing.

  • The build number has to rise. Sparkle compares CFBundleVersion, not the marketing version. It came from git rev-list --count HEAD, which is 1 on CI because checkout clones to depth 1, so every release shipped build 1 and no update was ever offered. Now derived from the version.
  • --timestamp=none fails notarization. Ad-hoc signing requires it and Apple rejects it, so branch on the identity.
  • Staple the app, not only the DMG. A ticket on the image does not travel with the app into /Applications, leaving a Mac that is offline at first launch with nothing to fall back on.
  • --deep is wrong once a framework is embedded. Sign inner-out: Sparkle's XPC services, Updater.app, Autoupdate, the framework, then the app.
  • Library validation needs matching Team IDs. An ad-hoc signature has none, so a local build dies loading Sparkle. Ad-hoc builds use a separate entitlements file that relaxes it; never ship that file.
  • hdiutil create -srcfolder needs a writable mount. Managed Macs and CI runners force disk images read-only. Use makehybrid.
  • Apple's timestamp server fails under load. A timestamp was expected but was not found. broke the v0.1.5 release on a Sparkle XPC service. The timestamp is mandatory for notarization, so signing retries with backoff rather than failing the release.
  • Build against the SDK the app will run on. 0.1.8 was built on the hosted macos-14 image, so it links against the macOS 14.5 SDK. On macOS 26 that binary grows its MenuBarExtra window and never shrinks it again: the window stays at the tallest list of the session (measured at 560x472) while the content draws its real height, and SwiftUI centres the content in the leftover space. The result is a menu floating ~140pt below the menubar with dead margin above and below, which reads as a padding bug and is not one. The same source built with the macOS 26 SDK goes 652 -> 157 across the same shrink. Check with vtool -arch arm64 -show-build <binary> | grep sdk before chasing a layout problem that only appears on one Mac.
  • The SDK stamp is set by build.sh and checked, because the toolchain stopped setting it. SwiftPM under Xcode 26 stamped the real SDK. Under Xcode 27 it stamps the deployment target, so a machine with the 27.0 SDK produced sdk 14.0 binaries and 0.1.32, 0.1.33 and 0.1.34 all floated. Nothing else showed it: builds passed, tests passed, screenshots matched. The stamps tell the story on their own: 0.1.30 and 0.1.31 read 26.5, the next three read 14.0, and Xcode was updated in between. build.sh passes -platform_version macos 14.0 <sdk> to the linker and then runs mac/check-sdk-stamp.sh, which fails unless every slice is stamped at least as new as the machine's own macOS; verify-dmg.sh runs the same check on the image, in CI and before a release publishes anything. If a future toolchain ignores the flag, the build fails instead of shipping.
  • The build Mac's toolchain is recorded, and a change turns CI red. Xcode updates itself on the runner and nothing in the repo changes when it does. mac/TOOLCHAIN holds the Xcode version, its build and the macOS SDK; mac/check-toolchain.sh --check runs first in CI and in the release, and a mismatch fails with the procedure: build, package, verify-dmg.sh, screenshots.sh --check, open the menu once with your eyes, then --write and commit. Red here means "look", not "broken". The alternative was learning about Xcode 27 from a user's screenshot three releases later.
  • A note in this file once blamed the layout for that, and was wrong. It said 0.1.33 floated "on the current SDK", which nobody had checked with vtool; it was stamped 14.0. WindowFitter was written on that belief. It stays as a second layer, but the version that shipped in 0.1.34 could not have worked: it fitted only when the measured height changed and skipped a window not yet on screen, and a menu reopening at a stale height is both. FittingView now also fits whenever its window becomes key or changes occlusion, and WindowFitterTests reproduces the stale-window case against a real NSWindow. Before believing a claim about which SDK a build used, read the stamp.
  • A Focus profile silences notifications and nothing in the API says so. UNUserNotificationCenter reported authorized, alert: enabled, sound: enabled; audio output was fine; and "Pop", "Pop.aiff" and .default were all silent. A Focus mode the app cannot see was filtering it. Before chasing a sound name or a permission, ask whether Focus is on: the app has no way to detect it and add() reports success either way.
  • The hook runs ~/.agent-inbox/bin/notify.sh, not your checkout. The app unpacks it from the bundle at launch, so a fix in the repo does nothing until a build carrying it is installed. A local build made before the fix will happily overwrite the good copy. Check with grep against the installed file, not the one you edited.
  • A file left by a transport you stopped using keeps sending. notify.sh posts to whatever it finds in ~/.agent-inbox/, so switching the app from Discord to ntfy left every session publishing to both for days with nothing to show it. The writer now retires the other transport's files.
  • The DMG window comes from a committed .DS_Store. Without one, Finder opens the image at whatever size and icon scale it last used, which is the cramped default window. The usual way to make one is to mount the image read-write and drive Finder over AppleScript, and this project cannot: that is the same read-only-mount problem that made create -srcfolder unusable. mac/dmg/make-ds-store.py writes the file directly, with no mount and no GUI, and makehybrid does carry dotfiles into the image. Re-run it only when the layout changes, and note that the names in it must match the staged files exactly or Finder silently falls back to automatic positions.
  • Finder drops a view-options dictionary it does not recognise, whole. The 0.1.24 image asked for 128pt icons and got Finder's default 64: the window settings in bwsp applied while every setting in icvp was discarded, with no error and nothing in a log. The file carried two keys Finder never writes (scrollPositionX/scrollPositionY, plus a stray ICVO) and was missing three it always writes (backgroundColorRed/Green/Blue). Match the key set of a .DS_Store Finder itself produced, which means pulling one out of a shipping DMG and diffing against it.
  • The background picture needs the pBBk bookmark, not only the alias. backgroundImageAlias inside icvp is the older half and Finder does not resolve it on its own. Both are written now.
  • AppleScript cannot tell you whether the background applied. background picture of icon view options reports NONE for images that certainly have one, checked against a third-party DMG as a control before trusting the reading. icon size and the window bounds do read back correctly, so verify what can be verified and have a person look at the rest.
  • The wire has a contract now, and two ways a message can reach the parser. notify.sh appends a versioned JSON line after the human lines and the old footer. Transport.splitFooter peels the last body line only if it looks like a footer, so normally the JSON stays in the body and the footer arrives inside it; but if any contract string contains · /, splitFooter peels the JSON into message.footer instead. MessageParser checks both places. Anyone rewriting splitFooter needs to keep both arrangements working, and MessageParser.peelFooter duplicates its two conditions on purpose so the model layer does not import the transport: change one, change both.
  • A contract with a null or unknown kind falls back whole. Half-applying a contract is worse than ignoring it, so the parser drops to the emoji heuristics for the entire message rather than filling what it can.
  • closing_words never contains a newline. Line ends are sentence ends by design. A test asserting a newline survives through .closing will fail on the reduction, not on jq.
  • HOST_LABEL in the environment is ignored. notify.sh assigns hostname -s unconditionally before sourcing config, so only ~/.agent-inbox/config can set it. HOST_LABEL=x ./notify.sh does nothing.
  • The dry-run output has no footer: line any more. The footer is the second-to-last body line and the JSON contract the last. tail -1 of a dry run is the contract.
  • Backoff after a failure starts at 2s, not 1s. consecutiveFailures is incremented before the sleep, so the ladder after failures is 2, 4, 8, 16, 32, 60. backoff(0), one second, is the wait after a healthy stream closes. Pinned in ReceiverTests; change the loop and the test together.
  • The receiver's watchdog ends the connection; it used to only flag it. The first streaming build set a flag at ten seconds but stayed inside the for try await until URLSession's 120s idle timeout threw, so the poll fallback began after two minutes, not ten seconds. Nothing caught it because nothing could: the loop called Task.sleep directly. Making time injectable (Sleeper) is what surfaced it. Stream and watchdog are now siblings in a task group and the first to finish decides.
  • receiver.restart() no longer restarts housekeeping. Reconnect and topic changes cycle the connection only; presence and expiry keep their own clock.
  • In receiver tests, settle on the last effect in the chain, not the first. settle(until:) returns when its condition holds; state set one hop later may not be there yet. And two sleeps woken by one advance resume in deadline order, but the main actor does not promise to run them in that order, so never assert cross-task ordering off a single advance.
  • Never use UserDefaults(suiteName:) in a test. It writes a real plist into ~/Library/Preferences, and cfprefsd rewrites it after the process has exited, so removePersistentDomain in tearDown does not stick. Two tests had left 826 of them on the maintainer's Mac before anyone looked. Use MemoryDefaults from IsolatedSettings.swift, and build models through IsolatedSettings.model(), which is the one way a test gets an app model that touches nothing real.
  • Adding a setting is two places. A property on SettingsValues and a line in its hand-written init(from:). Forget the second and the field decodes as its default forever, silently, because the decoder is deliberately lenient so an older snapshot never fails to load.
  • The gate for "no singletons" is static let shared, not .shared. grep -rn "\.shared" mac/Sources will always find Apple's own (NSWorkspace.shared, URLSession.shared). The one that means something is grep -rn "static let shared\|static var shared" mac/Sources, which must print nothing.
  • Sender config writes are change-gated now. A setter used to rewrite ~/.agent-inbox/config unconditionally; now sync() runs only when a sender-visible field actually changed. AppModel.start() still syncs once unconditionally, so a fresh launch always leaves the file current.
  • The delegate gets the model from the App, not from a global. AgentInboxApp.init builds the model and sets delegate.model on the adaptor, which has already constructed the delegate by then; verified with a throwaway package rather than assumed. The optional is guarded with a precondition, so a future refactor that reorders this fails at launch loudly, which is the right failure.
  • The senders and the app are checked against each other on every push. fixtures/wire/ holds the sender's real dry-run output for seven scenarios, regenerated by fixtures/wire/regenerate.sh with every varying input pinned; test-notify.sh fails if the committed files drift, and WireFixtureTests decodes every one through splitFooter then the parser and asserts .contract. A new scenario is three edits: regenerate.sh, run it, and the expected list in the Swift test, which asserts the directory holds exactly that set so a forgotten entry fails rather than being skipped.
  • Tests source notify.sh, they do not cut functions out of it. AGENT_INBOX_SOURCE_ONLY=1 source notify.sh returns before the stdin read and the dispatch. AGENT_INBOX_NOW pins "now" for the same reason. Both are environment-only seams; config cannot set them. Under a normal run the top-level return is a silent no-op, so the seam only means anything when sourced.
  • Tags inside code are ignored. An agent's report quoting `#mute` muted the maintainer's session for hours. strip_code drops fenced blocks and inline spans before matching; an unclosed fence swallows the rest of the prompt on purpose, biased toward not matching. Worth knowing when someone reports "my #notify did nothing".
  • The end-to-end test is the one that catches sender-to-app bugs. EndToEndTests runs the real notify.sh as a subprocess against FakeNtfyServer on 127.0.0.1 and asserts a row in the store. It would have caught the "$BODY💬" expansion bug; the test was proven by putting that bug back and watching it fail. It skips only when jq is missing.
  • Never build an AppModel in a test without IsolatedSettings.model(). UNUserNotificationCenter.current() aborts the whole test process in a bundle-less runner. IsolatedSettings passes a RecordingPoster; a bare AppModel(settings:defaults:) that delivers an item takes every test down with it.
  • In FakeNtfyServer, a dropped stream is a clean close on purpose. It writes the terminating chunk before cancelling, which is what makes stream() finish without throwing and the receiver reconnect on the 1s path. Cancelling without it is the "killed process" case: URLSession throws -1005, the receiver counts a failure and backs off 2s. Both are real ntfy behaviours; the tests exercise the first.
  • settle() cannot wait across a socket. It yields a fixed number of times, which is microseconds. Anything waiting on URLSession or a subprocess uses eventually(within:), a real-time deadline poll.
  • The installers refused nothing until they had tests. Both reported success over a corrupt settings.json because set -e does not fire inside an && list; install.sh wrote the hook path unquoted, so a checkout under a directory with a space produced hooks that exit 127 on every turn. Fifty-one checks in test-install.sh now, run in CI on Ubuntu and verified on macOS bash 3.2. Any change to how hooks are written must keep HookInstaller.swift and both shell installers in agreement; the test's app_shape_ok asserts it.
  • The README screenshots are generated, checked in CI, and re-committed by every release. mac/screenshots.sh --write regenerates docs/*.png from the views with every varying input pinned; --check renders into a temp directory and fails the push if docs/ differs or the README links an image nothing generates. It runs only on the self-hosted Mac: the images are byte-exact to that machine's SDK and fonts. The version in the pictures is a fixed 1.0.0 on purpose, because the release runner rewrites VERSION from the tag while the file in git stays behind, and a picture tracking either would fail the check on the first push after every release.
  • A settings capture must be sized to the view's own fitting height. Forcing a taller frame makes the scroll view fill the slack by scrolling and the picture comes out mid-page with the header and tabs cut off.
  • IsolatedSettings redirects HookInstaller.settingsURL too. It did not for a while, so an "isolated" model read this Mac's real ~/.claude/settings.json and the Machines screenshot carried a warning about the maintainer's own hooks. Two statics are redirected into the scratch directory; a third one would be a test reading something real again.
  • mac/verify-dmg.sh reads a built image back the way Finder and Sparkle will, and runs in CI after packaging and in the release before publishing. A probe that pipes its output through tail loses the exit code; call it bare. Stray /Volumes/Agent Inbox N mounts on a dev Mac are from hand probing, not from it: it detaches in a trap on every path.
  • Local shellcheck says nothing about CI's. Ubuntu's runner ships 0.9.0, a current Mac 0.11. SC2120 (an argument nothing passes) exists only in the older one and turned every push red for a week while the Mac said clean. chipdev has 0.8.0, which is the stricter check: run a script through it before trusting a green local run.
  • CI was red for a week and Discord never said so. The notify job called a shared action that treats a missing webhook as "nothing to do" and exits green, and the secret had never been set on this repo. Every failing run ended with a green Discord job whose log said no webhook configured. The notify jobs in ci.yml and release.yml are plain curl now, and a missing DISCORD_WEBHOOK_AGENT_INBOX_PRODUCTION fails the job instead of passing it. The channel is #agent-inbox-production, created by ideaplaces-devops/discord/setup-cicd-notifications.sh; the webhook is in Key Vault as discord-webhook-agent-inbox-production. A notifier that cannot notify must go red, because nobody opens the log of a green job.
  • The Stop hook fires before the transcript has the turn's last message. Measured on Claude Code 2.1.274: the file was seven lines short when the hook started and complete 200ms later. Reading the transcript there finds the previous message, so every row was one message in the past, most visibly on a short turn with no tool calls. The payload's last_assistant_message is the turn that ended; the transcript is only the fallback for older versions and for the Notification hook. Never sleep-and-reread to fix this: the hook must not block a session.
  • A background agent's report is a prompt, and a turn. Its <task-notification> fires UserPromptSubmit and then Stop, so before 0.1.36 every agent that finished cleared the session's row as if the user had read it and put up a fresh ✅: a session running five agents produced five rows, none with anything to do. The prompt branch now ignores those, and the stop branch holds while running_agents finds launches with no report. The report reaches the transcript in two shapes, a user entry when the session was idle and a queued_command attachment when it was mid-turn; matching only the first left half of all agents looking out forever. The hold is the one thing in the sender that outlives the hook: a detached recheck process, guarded by a nonce in state/<id>.hold. It does not read the transcript late: the launch's tool result is written before the model streams its closing reply, so it is already there when Stop fires.
  • A view change lands red on main and stays red until the next real push. CI compares docs/*.png byte for byte against pictures rendered on the Mac, and the images can only be regenerated there (mac/screenshots.sh --write). A view change pushed from anywhere else fails that step; the release job then renders and commits the new pictures, but a commit pushed with GITHUB_TOKEN never triggers a workflow, so no green run follows and the Discord failure post stays up. The order that works from a Linux box: push the change (red), tag the release (pictures land), then push the next real commit to main, which runs green on the fresh pictures and clears the post.
  • Test the binary you think you are testing. A --configure flag appeared to hang through several rounds of debugging because /Applications held the released build, which predated the flag.

Read the full file on GitHub · 387 lines

Changes

What this file has done since we first saw it

Hashed on every crawl. A supply-chain change to an agent config is a question of when, not whether, so the history is kept rather than the latest state alone.

  1. 10d ago Changed · +13 lines · +238 tokens per session 4a30ee4a4fd3
  2. 1mo ago Changed · +8 lines · +153 tokens per session d2b8553ac84b
  3. 1mo ago Changed · +77 lines · +1,128 tokens per session 40909727dee3
  4. 1mo ago Changed · +83 lines · +1,487 tokens per session scan A → B b8a9940397b6
  5. 1mo ago Changed · +90 lines · +1,603 tokens per session 600311c743fb
  6. 2mo ago First seen · 116 lines · 1,483 tokens per session scan A a3972c9b8f52

Subscribe to this mod's changes

agent-inbox CLAUDE.md is an instructions file published in the GitHub repository Ideaplaces/agent-inbox (3 stars, last pushed 10d ago), licensed MIT. It adds 6,092 tokens to every session, about $0.0244 per session on Opus 5.5. A static security scan graded it B with 2 findings (reads agent configuration directories, makes network calls). No closer match exists in the catalogue, so it is treated as the original; first seen 2026-08-31.

Related

Other instructions, from other repositories

vscode buildNext.instructions.md

Working notes and architecture documentation for the new esbuild-based build system in build/next. Use when making changes to the new build pipeline (transpile/bundle commands, NLS plugin, source-map handling, resource copying, or self-hosting watch tasks).

microsoft/vscode · 9,715 tokens

next.js AGENTS.md

AGENTS.md instructions for vercel/next.js, covering next.js development guide, codebase structure, monorepo overview, core package: packages/next and other important packages.

vercel/next.js · 7,461 tokens

vscode oss-third-party-notices.instructions.md

Instructions for microsoft/vscode, covering vs code oss third-party-notices pipeline, architecture, pipeline flow in ci, cache eligibility and rollout and applying the notice (cutover).

microsoft/vscode · 5,262 tokens

langchain AGENTS.md

AGENTS.md instructions for langchain-ai/langchain, covering global development guidelines for the langchain monorepo, corridor security analysis, project architecture and context, monorepo structure and development tools & commands.

langchain-ai/langchain · 4,680 tokens

vscode copilot-instructions.md

Copilot instructions for microsoft/vscode, covering vs code copilot instructions, project overview, root folders, core architecture (src/ folder) and built-in extensions (extensions/ folder).

microsoft/vscode · 2,750 tokens

deepseek-harness AGENTS.md

AGENTS.md instructions for deepseek-ai/deepseek-harness, covering agents.md, pre-stable apis and released session data, repository layout, commands and host sandbox failures.

deepseek-ai/deepseek-harness · 4,261 tokens