Capacitor Apps skill for Codex
Ship the web app natively without forking it. Codex treats the ios and android folders as committed source, keeps every @capacitor package on one major, and adds native features through a typed plugin interface with a web fallback so the app still runs in a browser. It writes app-local Swift and Kotlin plugins that always resolve or reject, handles back button, safe areas and CORS origins, and strips live-reload URLs before release.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- capacitor-apps Installs to ~/.agents/skills/capacitor-apps/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install capacitor-apps The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project capacitor-apps Installs to .agents/skills/capacitor-apps/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/capacitor-apps.zipunzip -o capacitor-apps.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) capacitor-apps PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $capacitor-apps, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Builds and maintains Capacitor hybrid apps (with Ionic, React, Vue, Angular or plain web) - capacitor.config, cap sync and the native iOS and Android projects, official and community plugins, writing custom Swift and Kotlin plugins, live reload, deep links, back button and safe areas, native HTTP, and Capacitor major upgrades. Use when wrapping a web app as an iOS and Android app, adding a native capability through a plugin, debugging a plugin that works on one platform only, or upgrading Capacitor.
Use it when
- Turning an existing web app into store-ready iOS and Android apps
- Adding a native capability that no official plugin covers
- A plugin works on one platform and fails on the other
- Upgrading Capacitor or its plugins
Use something else when
- The app is React Native with Expo: use react-native-expo
- The target is a desktop app with a web UI: use tauri-apps or electron-apps
How to check the result
npm run build plus npx cap sync succeeds cleanly, release builds run on physical devices with no dev server, and each plugin method's success, reject and permission-denied paths are exercised on both platforms.
The mistake it prevents
Leaving a live-reload server.url in the native config, so the release build tries to load a laptop's dev server.
Done well means: the web app runs unchanged in the browser and in the native shells, every
native capability goes through a typed plugin with a web fallback, the native projects are
committed and buildable from a clean checkout after npx cap sync, and nothing dev-only
(live reload URLs, cleartext) ships in a release build.
Before changing anything
- Read
AGENTS.md,package.jsonandcapacitor.config.ts(or.json):appId,webDir,server, plugin config. - Check versions:
npx cap doctorprints core, CLI and platform versions. All@capacitor/*packages must share the same major. - Inspect
ios/andandroid/: they are source code, committed, and may contain manual edits (Info.plist keys, manifest permissions, Gradle config). Never delete and regenerate them without asking. - Note the iOS dependency manager: Swift Package Manager (default for new projects on
recent Capacitor) or CocoaPods (
ios/App/Podfile). Plugin compatibility differs. npm install, Pods and Gradle downloads need network; ask for approval when sandboxed.
Daily commands
npm run build && npx cap sync # copy web build + update native deps; run after any plugin change
npx cap copy # web assets only (faster, no native dep changes)
npx cap open ios # Xcode
npx cap open android # Android Studio
npx cap run android --target <id> # build, install, launch
npx cap run ios --live-reload --host 192.168.1.20 --port 5173 # dev only
Live reload writes the dev server URL into the native config for that run. Before a
release build, confirm server.url is absent from capacitor.config.ts and
ios/App/App/capacitor.config.json / android/app/src/main/assets/capacitor.config.json.
Config essentials
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.field',
appName: 'Field',
webDir: 'dist',
plugins: {
SplashScreen: { launchAutoHide: false },
CapacitorHttp: { enabled: false }, // enable only if you want fetch patched globally
},
};
export default config;
Do not set server.cleartext: true or android.allowMixedContent: true for production.
Choosing a plugin
| Need | First choice |
|---|---|
| Camera, filesystem, preferences, share, haptics, status bar, keyboard, app lifecycle | Official @capacitor/* plugins |
| Push | @capacitor/push-notifications (FCM on Android, APNs on iOS) |
| Secure storage, biometrics, SQLite, in-app purchase | Maintained community plugin (check last release, open issues, SPM support) |
| Anything else or company-specific SDK | Custom plugin (below) |
Before adding a community plugin, check it supports your Capacitor major and your iOS dependency manager. An unmaintained plugin blocks every future upgrade.
Writing a custom plugin
For reuse across apps, scaffold a package with npm init @capacitor/plugin@latest. For an
app-only plugin, add the classes directly in the native projects:
// src/plugins/echo.ts
import { registerPlugin } from '@capacitor/core';
export interface EchoPlugin { echo(options: { value: string }): Promise<{ value: string }>; }
export const Echo = registerPlugin<EchoPlugin>('Echo', {
web: () => import('./echo-web').then((m) => new m.EchoWeb()),
});
// ios/App/App/EchoPlugin.swift
import Capacitor
@objc(EchoPlugin)
public class EchoPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "EchoPlugin"
public let jsName = "Echo"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise)
]
@objc func echo(_ call: CAPPluginCall) {
guard let value = call.getString("value") else { return call.reject("value is required") }
call.resolve(["value": value])
}
}
Register an app-local iOS plugin from a CAPBridgeViewController subclass (set it as the
storyboard’s view controller class):
class MainViewController: CAPBridgeViewController {
override open func capacitorDidLoad() { bridge?.registerPluginInstance(EchoPlugin()) }
}
// android/app/src/main/java/com/example/field/EchoPlugin.kt
@CapacitorPlugin(name = "Echo")
class EchoPlugin : Plugin() {
@PluginMethod
fun echo(call: PluginCall) {
val value = call.getString("value") ?: return call.reject("value is required")
call.resolve(JSObject().put("value", value))
}
}
// MainActivity.onCreate: registerPlugin(EchoPlugin::class.java) before super.onCreate(savedInstanceState)
- Do slow work off the main thread (Kotlin coroutines /
execute {}; SwiftTaskor a background queue), and touch UI only on the main thread. - Every method resolves or rejects exactly once; a forgotten path hangs the JS promise.
- Request runtime permissions through Capacitor’s permission API (
@Permissionaliases on Android,checkPermissions/requestPermissionson both), and add Info.plist usage strings. - Keep the TypeScript interface the single contract; write the web implementation (even if
it throws
unavailable) so the app runs in a browser.
Native behaviours to handle
- Android back button:
App.addListener('backButton', ({ canGoBack }) => canGoBack ? history.back() : App.exitApp()), or let Ionic’s router handle it. - Deep links:
App.addListener('appUrlOpen', ({ url }) => router.navigate(parse(url))); hosting and verification are covered in mobile-deep-links. - Safe areas:
viewport-fit=coverin the viewport meta andenv(safe-area-inset-*)paddings. - Keyboard: the Keyboard plugin’s
resizemode decides whether the webview shrinks; test forms near the bottom of the screen on both platforms. - CORS: requests from the webview originate from
capacitor://localhost(iOS) orhttps://localhost(Android). Allow those origins on your API, or route through CapacitorHttp.
Upgrading Capacitor
- Read the official upgrade guide for the target major; it lists required Xcode, Android Gradle Plugin, minimum iOS and Android SDK levels.
- Run
npx cap migrate(or the Capacitor VS Code extension’s migration), then review the diff inios/andandroid/line by line. - Upgrade every
@capacitor/*plugin and check community plugins for compatible releases before merging. - Build both platforms from clean and run the smoke test below.
Gotchas
- Forgetting
npx cap syncafter adding a plugin gives “plugin is not implemented on ios”. - Web storage (
localStorage, IndexedDB) can be cleared by the OS under storage pressure; use Preferences or SQLite for data that must persist. - Over-the-air live updates (Appflow, Capgo) may change web code only; changing native behaviour or the app’s primary purpose through OTA breaks store rules.
- Large JS bundles slow cold start more in a webview than in a browser; lazy-load routes.
Verification
-
npm run build && npx cap syncsucceeds with no warnings about mismatched versions - Release builds open on a physical iPhone and Android device with no dev server
- Each custom plugin method: success, reject path and permission denial tested on both
- Back button, deep link, rotate, background and resume behave correctly
- App still runs in a desktop browser with web fallbacks
Output
**Capacitor:** <version> iOS deps: SPM | CocoaPods
**Plugins:** <name -> official/community/custom, platforms>
**Native edits:** <files changed in ios/ and android/>
**Tested:** <device, OS, build type> -> result What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Capacitor Apps"
short_description: "Ship the web app natively without forking it"
default_prompt: "$capacitor-apps Wrap our Vite React app with Capacitor for iOS and Android" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.