justin․searls․co

Adding swift-format to your Xcode build

Xcode 16 and later come with swift-format baked in. Unfortunately, Xcode doesn't hook it up for you: aside from a one-off "Format File" menu item, you get no automatic formatting or linting on local builds—and zero guidance for Xcode Cloud.

Beginning with the end in mind, here's what I ended up adding or changing:

.
├── ci_scripts
│   ├── ci_pre_xcodebuild.sh
│   ├── format
│   └── lint
├── MyApp.xcodeproj
│   └── project.pbxproj
└── script -> ci_scripts/

Configuring swift-format

Since I'm new around here, I'm basically sticking with the defaults. The only rule I customized in my project's .swift-format file was to set indents to 2 spaces. Personally, I rock massive fonts and zoom levels when I work, so the default 4-space indent can result in horizontal scrolling.

{
  "indentation" : {
    "spaces" : 2
  }
}

Running swift-format in Xcode Cloud

Heads-up: if you wire swift-format into your local build you can skip this step. I'm laying it out anyway because sometimes it's handy to run these scripts only in the cloud—and starting with that flexibility costs nothing.

When you add custom scripts on Xcode Cloud, you can implement any or all of these three specially named hook scripts:

  • ci_scripts/ci_post_clone.sh
  • ci_scripts/ci_pre_xcodebuild.sh
  • ci_scripts/ci_post_xcodebuild.sh

If that feels limiting, it gets better: these scripts can call anything else inside ci_scripts. Because I always name my projects' script directory script/, I capitulated by putting everything in ci_scripts and made a symlink:

# Create the directory
mkdir ci_scripts
# Add a script/ symlink
ln -s ci_scripts script

Create the formatting & linting scripts

Next, I created (and made executable) my pre-build hook script, a format script, and a lint script:

# Create the scripts
touch ci_scripts/ci_pre_xcodebuild.sh ci_scripts/lint ci_scripts/format
# Make them executable
chmod +x ci_scripts/ci_pre_xcodebuild.sh ci_scripts/lint ci_scripts/format

With that, a pre-build hook (which only runs in Xcode Cloud) can be written like this:

#!/bin/sh
# ci_scripts/ci_pre_xcodebuild.sh

# See: https://developer.apple.com/documentation/xcode/writing-custom-build-scripts

set -e

./lint
./format

The lint script looks like this (--strict treats warnings as errors):

#!/bin/sh
# ci_scripts/lint

swift format lint --strict --parallel --recursive .

And my format script (which needs --in-place to know it should overwrite files) is here:

#!/bin/sh
# ci_scripts/format

swift format --in-place --parallel --recursive .

Note that the above scripts use swift format as a swift subcommand, because the swift-format executable is not on the PATH of the sandboxed Xcode Cloud environment.

(Why bother formatting in CI if it won't commit changes? Because I'd rather learn ASAP that something's un-formattable than be surprised when I run ./script/format later.)

Configuring formatting and linting for local builds

If you're like me, you'll want to lint and format on every local build as well:

In your project file, select your app target and navigate to the Build Phases tab. Click the plus (➕) icon and select "New Run Script Phase" to give yourself a place to write this little bit of shell magic:

"$SRCROOT/script/format"
"$SRCROOT/script/lint"

You'll also want to uncheck "Based on dependency analysis", since these scripts run across the whole codebase, it doesn't make sense to whitelist specific input and output files.

Finally, because Xcode 15+ sandboxes Run Scripts from the filesystem by default, you also need to go to the Build Settings tab of the target and set "User Script Sandboxing" to "No" in the target's Build Settings.

In MyApp.xcodeproj/project.pbxproj you should see the setting reflected as:

ENABLE_USER_SCRIPT_SANDBOXING = NO

And that's it! Now, when building the app locally (e.g. Command-B), all the Swift source files in the project are linted and formatted. As mentioned above, if you complete this step you can go back and delete your ci_scripts/ci_pre_xcodebuild.sh file.

Why is this so hard?

Great question! I'm disappointed but unsurprised by how few guides I found today to address issues like this, but ultimately the responsibility lies with Apple to provide batteries-included tooling and, failing that, documentation that points to solutions for common tasks.

Almost made it 2 hours working in Xcode before hitting my first Spinning Beachball of Death. I triggered it by scrolling a file listing.

Nostalgic!

Copied!

Starting my first new iOS app in over 15 years and on Day 1 I've spent more time debugging build errors in Keychain Access than I have in Xcode.

Some things never change.

Copied!

If my name were Albert, I would take advantage of the prevalence of sans-serif fonts and juice up that résumé. Tout what an expert in Al I was. Claim to be an Al-first developer. An Al-native. In the top 0.1% of the field with respect to Al.

Copied!
Breaking Change artwork

v40 - Go home Claude, you're drunk

Breaking Change

Video of this episode is up on YouTube:

Finally, a recommendation-heavy, full-mailbag show. Been a while.

New to the pod are achievements—watch your BreakingScore™ increase each time you write in to podcast@searls.co. The winner will receive nothing, probably.

Lynx:

Show me them show notes…

I was red-pilled by Claude Code over 4th of July weekend. Its performance was incredible. Ever since, it's been unbelievably incompetent by comparison.

Now the AI bros tell me Anthropic's servers get slammed during US business hours and Claude becomes 70% stupider. It's 4pm on a Friday and I only realized because Claude stopped fucking everything up. Great job, everyone.

Copied!

I am 40 damn years old and still believe it is a moral failing to take two trips to carry shit from one place to another, even if it's likely I'll drop everything.

Copied!

Invoice for my first Mac (2004)

Thanks to a bug in Apple Mail, my Gmail archive likes to revert to sort by ascending date every now and then. Today, I scanned through some of those early emails and stumbled upon this incredible artifact: the e-mail invoice from Apple.com for my first Mac. It was a build-to-order 12" iBook G4 in July 2004.

Besides being set in such carefully-coifed monospace plaintext, the invoice provides an almost hilarious level of detail and verbiage by today's standards. Also, it never gets old to marvel at how much computers have depreciated over time. A whopping $1,362.00 for a mid-tier build of Apple's smallest, cheapest laptop in 2004. That's $2,317.82 in 2025 dollars after inflation. Today, the cheapest laptop Apple.com will sell you is the M4 MacBook Air (in an unusually recommendable configuration), starting at $999.

Big thanks to my dad Fred for spoiling me with a second computer literally one year into college (for which he bought me a Dell in June of 2003), because I was so insistent on switching to the Mac. (I sure was an entitled piece of shit.) I really wish I'd managed to hold onto that thing—a lot of fond memories, looking back.

The full invoice follows:

Apple Computer, Inc.                               INVOICE RECEIPT

Please remit to:
FOR YOUR RECORDS ONLY

Customer Number    Invoice Number
900007             9212467049

Reference Date     07/14/04

Amount Due         .00


Please Reference Apple's Invoice Number on Your Remittance

Sold To:                                 Ship To:
Fred Searls                              Justin Searls
REDACTED                                 REDACTED
TRENTON MI  48183                        GRAND RAPIDS MI  49504
USA                                      USA

________________________________________________________________________________
Customer Number    Customer P.O. Number    Sales Order Number
900007             7346758400              7010461968

Invoice Number     Invoice Date            Terms
9212467049         07/14/04                Credit Card
________________________________________________________________________________

Item Product   Product                        Total   Total   Unit      Extended
     Number    Description                    Ordered Shipped Price     Price
________________________________________________________________________________

002 Z0A704KVH  IBOOK 12.1/1.GHZ/768/60G/COMB      1     1  1,362.00     1,362.00
               Original ordered material was Z0A7
               SerialNo.: ( UV42906LR73 )

               The unit above contains the following options:

               Memory                    065-5001  768MB DDR266
               Hard Drive                065-5004  60GB ULTRA ATA DRIVE
               Optical Drive             065-5006  COMBO DRIVE
               Airport                   065-5009  Airport Extreme Card
               BlueTooth                 065-5011  Internal BlueTooth Module
               Custom SW I               065-4683  Not Applicable
               Keyboard/Mac OS Language  065-5012  Keyboard/Mac OS

________________________________________________________________________________

Subtotal              1,362.00

Tax                      81.72

Shipping Charges

TOTAL  USD            1,443.72
                                    DO NOT PAY
________________________________________________________________________________
Questions? Call (800) 676-2775 Mon-Fri 8am-9pm, Sat-Sun 9am-6pm CT
________________________________________________________________________________
Salesperson   Contact  Entry Date  Ship Date  Routing   Waybill Number
              BD       07/11/04    07/13/04   FEDERAL EX658584351832
________________________________________________________________________________
Shipped From:
F/G Distribution Center
Elk Grove, Ca 95758



               Web Order Number: W8731448

               Billed To: Credit Card


________________________________________________________________________________
After Remitting Payment Retain This Portion Of Invoice For Your Records.

Please See Below For Terms And Conditions Pertaining To This Order.
________________________________________________________________________________

Apple Computer, Inc.
________________________________________________________________________________
TERMS & CONDITIONS OF SALE


ORDER STATUS  For order status information, you may visit
http://www.apple.com/OrderStatus or navigate to http://store.apple.com/ and click
the "Your Account" button to view the status of your order.

U.S. SALES ONLY  The Apple Store sells and ships products only within the
continental United States, Alaska, and Hawaii. No shipments can be outside the
United States. You may not export any products purchased at the Apple Store.

SALES TO END USERS ONLY  The Apple Store sells and ships products to end user
customers only.

RETURN & REFUND POLICY   If you are not satisfied with your Apple purchase of a
pre-built product, please call 1-800-676-2775 for a Return Material Authorization
(RMA) request within 10 business days of the receipt of the product. If the item
is returned unopened in the original box, we will exchange it or offer you a
refund based on your original method of payment. The product must be returned to
the Apple warehouse within 10 business days of the issuance of the RMA. All
products must be packed in the original, unmarked packaging including any
accessories, manuals, documentation and registration that shipped with the
product. A 10% open box fee will be assessed on any opened hardware or accessory.
If you purchased your order using an Apple Instant Loan or an Apple Business
Lease, you may be asked to provide a major credit card (Visa, MasterCard,
American Express, or Discover) for Apple to assess the 10% open box restocking
fee.

Please note that Apple does not permit the return of or offer refunds for the
following products:

  1. Product that is custom configured to your specifications
  2. Opened memory
  3. Opened software
  4. Electronic software downloads

NOTE: Apple recommends that you (1) use a carrier that offers shipment tracking
for all returns and (2) either insure your package for safe return to Apple or
declare the full value of the shipment so that you are completely protected if
the shipment is lost or damaged in transit. If you choose not to (1) use a
carrier that offers tracking and (2) insure or declare the full value of the
product, you will be responsible for any loss or damage to the product during
shipping. Please note that the United States Postal Service (USPS) offers limited
tracking capabilities and that there is a 30-calendar-day waiting period before
the USPS will initiate a trace.

DEFECTIVE ITEMS  If you discover what you believe is a product defect for any
Apple-branded product, please contact Apple Care Technical Support at
1-800-APL-CARE (275-2273). Such a defect, if any, is covered under the terms of
your product's warranty. Please refer to the warranty information and other
supporting documentation that came with your product. (See Product Warranty
section below for specific information about Apple's product warranties.)

If you discover what you believe is a product defect for any third-party product,
please contact the manufacturer directly for information regarding the
manufacturer's warranty.

PROOF OF PURCHASE  This receipt is your proof of purchase from Apple.

CUSTOM-CONFIGURED PRODUCT  We are pleased to offer product that is
custom-configured to your specifications, and we encourage you to review your
order carefully. Since the product is built to your specifications, the order
cannot be changed, modified, or canceled once your order is in production.

SUPPORT PRODUCTS  Support products (such as the AppleCare Protection Plan) are
subject to the terms and conditions that accompany those products. By requesting

services under those products or completing and returning to Apple any
accompanying enrollment forms, you agree to the terms and conditions that apply
to those products. Those terms and conditions take precedence over any
inconsistent provisions in these Terms & Conditions of Sale.

SALES TAX  Apple Store purchases will include sales tax based on the ship-to
location and the sales tax rate in effect at the time of shipping. If you phone
in your order, the Apple Store sales representative will provide the final dollar
total of your order including tax and any applicable shipping charges at the time
you place your order. If the sales tax rate for the state to which your order is
being shipped changes before the product is shipped, the new tax rate in effect
at the time of shipment will apply. The proof of purchase that Apple mails to you
will include any applicable sales tax.

PRICES  The Apple Store endeavors to offer you competitive prices on current
Apple products and selected refurbished and clearance products Your total order
price will include the price of the product (on the day of shipping) plus any
applicable sales tax and shipping charges. Apple reserves the right to change
prices for products displayed at the Apple Store at any time.

Should Apple reduce its price on any shipped product within 10 calendar days of
shipment, you may contact Apple Sales Support at 1-800-676-2775 to request a
refund or credit of the difference between the price you were charged and the
current selling price. To receive the refund or credit you must contact Apple
within 14 business days of shipment.

PRODUCT AVAILABILITY  Given the popularity of some products, Apple may restrict
the number of such items that you may purchase. Any product limit restrictions
will be posted on the Apple Store web site.  This information is subject to
change.

PRODUCT WARRANTY  The sole warranty for Apple-branded product shall be Apple's
standard Limited Warranty as set forth in the documentation that accompanies each
Apple product.

Non-Apple-branded/Third-party products are sold "AS IS" by the Apple Store, but
may be accompanied by their manufacturers' standard warranties. "AS IS" products
are sold by Apple as is, where is, and with all faults, and without express or
implied warranties from Apple. If you have questions about any manufacturers'
warranties that accompany such products, please call 1-800-APL-CARE (275-2273)..

OTHER TERMS AND CONDITIONS
- Apple is not responsible for typographical errors.
- Apple reserves the right to change the terms and conditions of sale at the
Apple Store at any time.
- Title and risk of loss to all products will pass to you on delivery.
- All sales at the Apple Store are governed by California law, without giving
effect to California's conflict of law provisions.
- No Apple employee or agent has the authority to vary any of the Apple Store's
policies or the terms and conditions governing any sale.
- Additional terms and conditions may apply to Education customers purchasing for
personal use. Please refer to the Education Individual sales policies located at:
http://store.apple.com/Catalog/US/Images/salespoliciesEdIndividual.html


02-CONS-05-09-03

TLDR is the best test runner for Claude Code

A couple years ago, Aaron and I had an idea for a satirical test runner that enforced fast feedback by giving up on running your tests after 1.8 seconds. It's called TLDR.

I kept pulling on the thread until TLDR could stand as a viable non-satirical test runner and a legitimate Minitest alternative. Its 1.0 release sported a robust CLI, configurable (and disable-able) timeouts, and a compatibility mode that makes TLDR a drop-in replacement for Minitest in most projects.

Anyway, as I got started working with Claude Code and learned about how hooks work, I realized that a test runner with a built-in concept of a timeout was suddenly a very appealing proposition. To make TLDR a great companion to agentic workflows, I put some work into a new release this weekend that allows you to do this:

tldr --timeout 0.1 --exit-0-on-timeout --exit-2-on-failure

The above command does several interesting things:

  • Runs as many tests in random order and in parallel as it can in 100ms
  • If some tests don't run inside 100ms, TLDR will exit cleanly (normally a timeout fails with exit code 3)
  • If a test fails, the command fails with status code 2 (normally, failures exit code 1)

These three flags add up to a really interesting combination when you configure them as a Claude Code hook:

  • A short timeout means you can add TLDR to run as an after-write hook for Claude Code without slowing you or Claude down very much
  • By exiting with code 0 on a timeout, Claude Code will happily proceed so long as no tests fail. Because Claude Code tends to edit a lot of files relatively quickly, the hook will trigger many randomized test runs as Claude works—uncovering any broken tests reasonably quickly
  • By exiting code 2 on test failures, Claude will—according to the docs—block Claude from proceeding until the tests are fixed

Here's an example Claude Code configuration you can drop into any project that uses TLDR. My .claude/settings.json file on todo_or_die looks like this:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|MultiEdit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "bundle exec tldr --timeout 0.1 --exit-0-on-timeout --exit-2-on-failure"
          }
        ]
      }
    ]
  }
}

If you maintain a linter or a test runner, you might want to consider exposing configuration for timeouts and exit codes in a similar way. I suspect demand for hook-aware CLI tools will become commonplace soon.

Why I haven't started yet

This is a copy of the Searls of Wisdom newsletter delivered to subscribers on July 15, 2025.

Greetings everyone, and welcome to the middle of July in Orlando, where it's too damn hot outside and too damn cold inside. Thank you for joining our Q2 Performance Review of the Justin Business Unit at Searls LLC. Before I share status updates on this year's strategic initiatives and dig into how we're tracking against our KPI benchmarks, here are a few highlights regarding our output over the past month:

  • The latest pre-release build of POSSE Party adds support for four new platform integrations, a 200% increase over the previous version
  • I started a new interview series on called Hotfix on the Breaking Change feed, and its inaugural episode is getting rave reviews. When you factor in March's launch of Merge Commits, podcast series are up 300% compared to the year-ago quarter
  • "Full-breadth Developers" generated tens of thousands of organic impressions in its first 24 hours of publication, marking the fastest growth of a buzzword-defining post in justin.searls.co history
  • Speaking of the website, a new suite of automations has been implemented that add support for scheduling posts in the future, fetching social images for outbound links, normalizing typographical inconsistencies, and pissing off most of my followers by spewing an endless stream of my Japanese restaurant reviews

There were some bittersweet notes this month, as well. The Walt Disney Company decided to conclude its partnership with Searls LLC with the closure of Tom Sawyer Island, ending a 16-year tradition of my posing in front of the name "Tom" on this fence:

One last visit to the Justin ♥ Becky sign before Disney closed Tom Sawyer Island

What's with all this corporate year-in-review stuff? Well, Becky's latest podcast prompted me to consider doing my own, "6 months down, 6 to go," retrospective on 2025. This is me leaning in.

The verdict is in, and I've decided to give myself a failing grade.

To be continued…

Breaking Change artwork

v39.0.1 - Dave Mosher: Use AI in Anger

Hotfix

Video of this episode is up on YouTube:

Welcome to the first episode of 🔥Hotfix🔥! Breaking Change's first show-within-a-show, wherein I let somebody else talk for once. Each episode will show up as a patch release in the Breaking Change feed and feature guests with Hot takes about a relevant issue and a clear fix in mind for what we can do about it.

That first guest is a long-time collaborator and top 5 all-time colleague of mine named Dave Mosher, who's here to drop some truth bombs labeled "agentic coding" on the unsuspecting populace.

My secret mission on each of these is to lean into the show's E-for-explicit tag and try to get the guest to say something that could get them fired. I failed this time, but in fairness to me, Dave is Canadian.

We'd love to get your feedback to podcast@searls.co — I'll read it all and flag relevant questions and comments for the next Breaking Change.

You can follow Dave Mosher online at:

A handful of things we mentioned:

Notify your iPhone or Watch when Claude Code finishes

I taught Claude Code a new trick this weekend and thought others might appreciate it.

I have a very bad habit of staring at my computer screen while waiting for it to do stuff. My go-to solution for this is to make the computer do stuff faster, but there's no getting around it: Claude Code insists on taking an excruciating four or five minutes to accomplish a full day's work. Out of the box, claude rings the terminal bell when it stops out of focus, and that's good enough if you've got other stuff to do on your Mac. But because Claude is so capable running autonomously (that is, if you're brave enough to --dangerously-skip-permissions), that I wanted to be able to walk away from my Mac while it cooked.

This led me to cobble together this solution that will ping my iPhone and Apple Watch with a push notification whenever Claude needs my attention or runs out of work to do. Be warned: it requires paying for the Pro tier of an app called Pushcut, but anyone willing to pay $200/month for Claude Code can hopefully spare $2 more.

Here's how you can set this up for yourself:

  1. Install Pushcut to your iPhone and whatever other supported Apple devices you want to be notified on
  2. Create a new notification in the Notifications tab. I named mine "terminal". The title and text don't matter, because we'll be setting custom parameters each time when we POST to the HTTP webhook
  3. Copy your webhook secret from Pushcut's Account tab
  4. Set that webhook secret to an environment variable named PUSHCUT_WEBHOOK_SECRET in your ~/.profile or whatever
  5. Save the shell script below
  6. Use this settings.json to configure Claude Code hooks

Of course, now I have a handy notify_pushcut executable I can call from any tool to get my attention, not just Claude Code. The script is fairly clever—it won't notify you while your terminal is focused and the display is awake. You'll only get buzzed if the display is asleep or you're in some other app. And if it's ever too much and you want to disable the behavior, just set a NOTIFY_PUSHCUT_SILENT variable.

The script

I put this file in ~/bin/notify_pushcut and made it executable with chmod +x ~/bin/notify_pushcut:

#!/usr/bin/env bash

set -e

# Doesn't source ~/.profile so load env vars ourselves
source ~/icloud-drive/dotfiles/.env

if [ -n "$NOTIFY_PUSHCUT_SILENT" ]; then
    exit 0
fi

# Check if argument is provided
if [ $# -eq 0 ]; then
    echo "Usage: $0 TITLE [DESCRIPTION]"
    exit 1
fi

# Check if PUSHCUT_WEBHOOK_SECRET is set
if [ -z "$PUSHCUT_WEBHOOK_SECRET" ]; then
    echo "Error: PUSHCUT_WEBHOOK_SECRET environment variable is not set"
    exit 1
fi

# Function to check if Terminal is focused
is_terminal_focused() {
    local frontmost_app=$(osascript -e 'tell application "System Events" to get name of first application process whose frontmost is true' 2>/dev/null)

    # List of terminal applications to check
    local terminal_apps=("Terminal" "iTerm2" "iTerm" "Alacritty" "kitty" "Warp" "Hyper" "WezTerm")

    # Check if frontmost app is in the array
    for app in "${terminal_apps[@]}"; do
        if [[ "$frontmost_app" == "$app" ]]; then
            return 0
        fi
    done

    return 1
}

# Function to check if display is sleeping
is_display_sleeping() {
    # Check if system is preventing display sleep (which means display is likely on)
    local assertions=$(pmset -g assertions 2>/dev/null)

    # If we can't get assertions, assume display is awake
    if [ -z "$assertions" ]; then
        return 1
    fi

    # Check if UserIsActive is 0 (user not active) and no prevent sleep assertions
    if echo "$assertions" | grep -q "UserIsActive.*0" && \
       ! echo "$assertions" | grep -q "PreventUserIdleDisplaySleep.*1" && \
       ! echo "$assertions" | grep -q "Prevent sleep while display is on"; then
        return 0  # Display is likely sleeping
    fi

    return 1  # Display is awake
}

# Set title and text
TITLE="$1"
TEXT="${2:-$1}"  # If text is not provided, use title as text

# Only send notification if Terminal is NOT focused OR display is sleeping
if ! is_terminal_focused || is_display_sleeping; then
    # Send notification to Pushcut - using printf to handle quotes properly
    curl -s -X POST "https://api.pushcut.io/$PUSHCUT_WEBHOOK_SECRET/notifications/terminal" \
         -H 'Content-Type: application/json' \
         -d "$(printf '{"title":"%s","text":"%s"}' "${TITLE//\"/\\\"}" "${TEXT//\"/\\\"}")"
    exit 0
fi

Claude hooks configuration

You can configure Claude hooks in ~/.claude/settings.json:

{
  "hooks": {
    "Notification": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/bin/bash -c 'json=$(cat); message=$(echo \"$json\" | grep -o '\"message\"[[:space:]]*:[[:space:]]*\"[^\"]*\"' | sed 's/.*: *\"\\(.*\\)\"/\\1/'); $HOME/bin/notify_pushcut \"Claude Code\" \"${message:-Notification}\"'"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "$HOME/bin/notify_pushcut \"Claude Code Finished\" \"Claude has completed your task\""
          }
        ]
      }
    ]
  }
}

If candidates are cheating your interview process by using AI, the solution is not to quiz them on computer science topics. It's to pair with them and watch them cook! If they can cheat with AI better than you can, hire them.

Copied!