Skip to content
ostiumsPublic

About

Long press any element of your iOS app in the simulator, say what is wrong, and the report lands in your Claude Code session

Resources

Stars

60 stars

Watchers

1 watching

Forks

Repository files navigation

FixKit

Long press anything in your iOS, React Native or Flutter app in the simulator, say what's wrong, and Claude Code or Codex fixes it.

iOS 17+ Swift 6 SwiftUI, UIKit, React Native and Flutter MIT

Long presses in the simulator send reports to Claude Code in the terminal: a shifted button, a corner radius, a single-colour chart and the tab bar, each fixed while the app relaunches

Watch the demo in full quality (MP4, 47 s)

Quick start · Codex · SwiftUI · UIKit · React Native · Flutter · How it works · Try it on Tally

  1. Long press an element and type what's wrong.
  2. The report lands in your agent session: your words, the element, a screenshot.
  3. The agent fixes the code and relaunches the app. A banner tracks it: queued → fixing → rebuilding → live.

FixKit has two parts:

  • In the app: a debug-only library that sends reports. The FixKit Swift package for SwiftUI and UIKit, the fixkit npm package for React Native, the fixkit Dart package for Flutter.
  • In the agent: the fixkit plugin for Claude Code and Codex. Its local server, the receiver (127.0.0.1:4747), turns each report into a prompt.

Quick start

For a SwiftUI or UIKit app with Claude Code. With Codex as the agent, swap in the steps from Codex. For a React Native or Flutter app, see React Native or Flutter.

You need: Claude Code 2.1.287+ or Codex CLI 0.159+, Node.js 18.2+, Xcode 16.3+ and an iOS 17+ simulator. FixKit works in the simulator only.

Recommended:

  • AXe names the pressed element. If XcodeBuildMCP runs through npx, FixKit uses the AXe bundled with it; otherwise brew install cameroncooke/axe/axe, or point FIXKIT_AXE at a copy. Without AXe, reports carry only the screenshot and the touch point.
  • XcodeBuildMCP rebuilds and relaunches in one call.

1. Install the plugin

claude plugin marketplace add ostiums/fixkit
claude plugin install fixkit@fixkit

2. Add the package

Xcode: File › Add Package Dependencies › https://github.com/ostiums/fixkit, rule Branch: main. Or in Package.swift:

.package(url: "https://github.com/ostiums/fixkit", branch: "main")

3. Add one line to the app

import FixKit

// SwiftUI, on the root view
WindowGroup {
    ContentView()
        .fixKitHost()
}

// UIKit, on the main window once it is visible
window.makeKeyAndVisible()
window.fixKitHost()

4. Run it

  1. Add .fixkit/ to .gitignore: reports and screenshots go there.

  2. Let Claude read screenshots without asking, in .claude/settings.json:

    { "permissions": { "allow": ["Read(./.fixkit/**)"] } }
  3. Start claude in the project's root and run a debug build in the simulator.

  4. Long press an element, type what's wrong, press Return.

The Fix queue pane lists reports and their status. It opens by itself in terminals 144+ columns wide, or with /fix-queue.

Codex

Same package, same line in the app, same .fixkit/ in .gitignore. Only the plugin setup differs:

codex plugin marketplace add ostiums/fixkit
codex plugin add fixkit@fixkit
  1. Start codex in the project's root and trust the plugin's hooks when asked (or later in /hooks). Untrusted hooks don't run.
  2. Send any message before the first long press. FixKit starts with the session's first turn; until then the app shows "not listening". To skip this, start with a message: codex "FixKit". A resumed session works the same way.
  3. Run a debug build, long press, type, press Return.

Differences from Claude Code:

  • No pane. Progress shows only in the app's banners (which say "Claude Code" with either agent). Log: $TMPDIR/fixkit-codex.log.
  • Only in app projects: a folder, or one above it up to the git root, with an Xcode project, a Package.swift, a React Native package.json or a Flutter pubspec.yaml. Elsewhere FixKit does nothing.
  • Needs Codex's shared app-server (the default). Sessions started with codex --no-daemon get no reports.
  • Ending the session stops FixKit. Reports already queued run when you resume, but without a banner.

Pointing at the right code

Nothing has to be marked. SwiftUI views carry their own lines; UIKit views are named after their properties.

SwiftUI

Every view reports the line its code starts on, the app's view around it and where that view is used:

[fix r1] TransactionRow › Text · Features/Home/TransactionRow.swift:29 · used at Features/Home/HomeView.swift:60 · StaticText "−€7.40" near "Coffee" · .fixkit/reports/r1.png
  • The innermost view wins: a press on a row's amount names the amount's Text, a press between the row's labels names its HStack.
  • Backgrounds and overlays step aside: shapes, colours and spacers lose to the content they sit behind or around.
  • .fixable("name") is optional: it names the element (card.holderName › Text) and lets scripted recordings press it. Names may carry data: .fixable("transaction.amount.\(merchant)"). The name goes to the view it is on and the views written inside it, up to the nearest view of the app's own: a .fixable on a .background or a ZStack layer names that layer, not the text over it.
  • Where the code is, not where the view is drawn: "used at" follows the code, so a view moved by .offset or a header pinned over a row is reported where it is written.
  • .fixScreen("Home") names the screen. With a TabView: .fixScreen(selectedTab.rawValue).

FixKit adds a ViewBuilder.buildExpression in Debug builds: the compiler passes each view's file and line through it, and SwiftUI hands them back with the view's frame, the way Xcode Previews selects views. Release builds compile none of it.

A builder that must return one concrete type stops compiling in Debug, because every view in it comes back wrapped:

cannot convert return expression of type 'ModifiedContent<Text, FixMarkModifier>' to return type 'Text'

@ViewBuilder title: () -> Text or @ViewBuilder var label: Text do this. Return some View there, drop @ViewBuilder where a single view is returned, or turn the feature off (below). A builder whose result must be Sendable gets a warning before iOS 26.

Where it falls short:

  • A body with an explicit return is named where it is used, and so is a view from another package.
  • A row scrolled entirely under a bar drawn by the same SwiftUI hierarchy, such as a custom tab bar, can win over the bar where the bar has nothing of its own. Accessibility and the screenshot in the report show what was pressed.
  • The MemberImportVisibility upcoming feature needs import FixKit in every file with views.
  • To turn it off, depend on FixKit without its default trait: traits: [] on the dependency in Package.swift, or the same in the package's traits in Xcode. Without it, an unmarked view is named by accessibility and a .fixable view by its mark's line. It relies on SwiftUI's underscored API for Previews, so this is also the way out if an SDK drops it.

UIKit

No marks needed: FixKit finds the property that holds the pressed view, in the view controller, a custom view or a cell:

[fix r1] ProfileViewController.nameLabel · UILabel "Alex Morgan" · .fixkit/reports/r1.png
  • Outlets, lazy vars and arrays (buttons[2]) are found too; a press on a button's label names the button.
  • The nearest owner wins: a label inside a custom card is named after the card's property.
  • A view without a property is reported by class and screen: UILabel "Alex Morgan" · ProfileViewController screen.
  • A press inside a sheet never names a view beneath it.

For an exact line, mark the view:

let payButton = UIButton(configuration: .filled()).fixable("checkout.pay")

React Native

The fixkit npm package: TypeScript, no native code, works in Expo Go and development builds. Needs React Native 0.80+ (React 19.1) with the New Architecture.

In Expo Go, a long press on a card holder's name cut off to "Alex…" sends "The name is cut off" to Claude Code; Claude removes a fixed width in WalletCard.tsx and Fast Refresh shows "Alex Morgan" with the banner "Fixed by Claude Code"

Install the plugin (Codex), then:

npm install --save-dev fixkit
import { FixKitHost } from 'fixkit'

export default function App() {
  return (
    <FixKitHost>
      <RootNavigator />
    </FixKitHost>
  )
}

Run it as usual; no Xcode, Podfile or app.json changes:

Project Command
Expo Go npx expo start --ios
Expo development build npx expo run:ios once, then npx expo start --ios
React Native CLI npx react-native run-ios
  • Start the agent in the folder with package.json. Above it, Claude Code treats the app as native, and Codex may not start FixKit.
  • Keep Metro running. Fixes go live through Fast Refresh, no rebuild. Native changes (Podfile, Info.plist, config plugins) are rebuilt with the command above.
  • Release builds leave FixKit out.

Reports name the components, the JSX line and where the component is used:

[fix r1] WalletCard › Text · src/WalletCard.tsx:11 · used at App.tsx:15 · StaticText "Alex Morgan" · .fixkit/reports/r1.png
  • testIDs appear as #id.
  • The pressed button stays still: Pressable, Touchable*, Button and pressable Text don't fire while FixKit holds a press. It wraps React Native's private Pressability for that, so Metro warns once about a deep import. Gesture Handler buttons aren't held.
  • Lines may be a few off after Fast Refresh changes a file, until the next reload.
  • Boot one simulator per iOS version: with several, the receiver can't tell which runs the app, and reports come without a screenshot.
  • Not covered yet: native Modals and natively presented screens, nested <FixKitHost>, Android.

Try it on the example app:

cd react-native/example && npm install && npx expo start --ios
claude --plugin-dir ../../mod    # in the same folder

Flutter

The fixkit Dart package: Dart only, no native code, no dependency beside the Flutter SDK, so nothing changes in the Podfile or the Xcode project. Needs Flutter 3.35+.

A long press on the wallet balance sends "Make the balance green" to Claude Code; Claude changes the colour in wallet_card.dart, calls hot_reload, and the balance turns green under the banner "Fixed by Claude Code"

Install the plugin (Codex), then add the package to pubspec.yaml:

dependencies:
  fixkit:
    git:
      url: https://github.com/ostiums/fixkit.git
      path: flutter
import 'package:fixkit/fixkit.dart';

void main() => runApp(const FixKitHost(child: MyApp()));

Run it with flutter run in the iOS simulator, from a terminal, VS Code or Android Studio.

  • Start the agent in the folder with pubspec.yaml. That is how the plugin knows the app is Flutter.
  • No rebuild. Saving a file does not hot reload a Flutter app, so the agent asks for it: in Claude Code with the mod's hot_reload tool, in Codex with a POST to the receiver's /reload. Both go through the VM service flutter run serves, the one DevTools' reload button calls. A compile error comes back to the agent; a change hot reload can't apply (initState, globals, main) gets a hot restart.
  • In Codex, the POST needs network access, which Codex's default sandbox blocks, even to 127.0.0.1: approve the command when Codex asks, or allow it for the session with codex -c sandbox_workspace_write.network_access=true.
  • Release builds leave FixKit out: FixKitHost returns its child unless kDebugMode, a constant the compiler folds. On Android, the web and the desktop it returns its child too, also with debugDefaultTargetPlatformOverride set to iOS.

Reports name the app's widget around the pressed one, the line of its constructor call and where that widget is used:

[fix r1] WalletCard › Text · lib/wallet_card.dart:20 · used at lib/main.dart:34 · StaticText "€12,480.50" · .fixkit/reports/r1.png
  • Widgets from Flutter and pub.dev packages are skipped for the app's own widget around them.
  • The pressed button stays still: FixKit's long press wins Flutter's gesture arena at 450 ms, before a widget's own 500 ms long press, so the widget under the finger neither taps nor long-presses. An app's long press shorter than 450 ms wins over FixKit's.
  • Lines stay current: a hot reload rebuilds the widgets from the new code.
  • Not covered yet: a widget built in a helper method points into that method, not at its caller; Android.

Try it on the example app:

cd flutter/example
claude --plugin-dir ../../mod    # in this folder
flutter run                      # in a second terminal, with the iOS simulator booted

How it works

app (FixKit) ──POST /report──▶ receiver (node, 127.0.0.1:4747) ──AXe──▶ simulator
      ▲                               │
      │ GET /status, POST /launched   ▼
      └────────── statuses ◀── Claude Code mod  or  Codex hooks
  • Long press (0.5 s) runs alongside the app's gestures, so buttons and scroll views keep working.
  • Composer and banners live in a separate window above the app: they cover sheets and stay out of screenshots.
  • The receiver names the deepest element under the touch and saves the accessibility tree to .fixkit/reports/<id>.ax.json.
  • Claude Code: the mod submits the report as a prompt and tracks its turn.
  • Codex: the receiver queues the prompt with codex queue; hooks report the turn's progress.
  • "Live" means the app relaunched while the agent worked: via XcodeBuildMCP, xcodebuild, Xcode's Run, Fast Refresh or a Flutter hot reload.
  • One session at a time: the newest session takes port 4747.
  • Release builds compile FixKit out, whatever flags the app sets.

Try it on Tally

Tally/ is a SwiftUI wallet with four seeded UI bugs:

./scripts/reset-demo.sh          # restores the bugs, builds and launches Tally
claude --plugin-dir ./mod        # the plugin from this checkout
Where Long press Say
Home the Send button button is shifted
Home the Top up button corners don't match the others
Home or Cards the card holder name name is cut off
Home or Activity a green-category amount, such as the salary income should be green

The bugs come from the git tag demo-start. Scripts use the iPhone 18 Pro simulator; set SIMULATOR for another.

Scripted recordings

FixKit can play reports from a script, for screen recordings. Enable it:

xcrun simctl spawn booted defaults write <bundle id> FixKitDirector -bool YES

Then serve steps from http://127.0.0.1:4748/next, one JSON object per request (204 when done):

{"press": "card.holderName", "text": "The name is cut off"}
{"at": [321, 686], "text": "Income should be green"}
{"scroll": 260}
{"defaults": {"selectedTab": "Activity"}}

press targets a .fixable name, at a point. defaults stores values for the next launch.

Development

scripts/test.sh runs every check: plugin validation, the mod, receiver and Codex tests, the React Native tests (Node.js 22.18+), the Flutter package's tests and analysis (when flutter is installed), the Swift package tests and Tally's Debug and Release builds.

License

MIT, see LICENSE.

About

Long press any element of your iOS app in the simulator, say what is wrong, and the report lands in your Claude Code session

Resources

Stars

60 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages