Xcode Projects for the Command Line and Coding Agents Apple's Xcode 27.2 beta 2 introduced a new JSON-based project format stored in project.xcproj, replacing the project.pbxproj property list, and beta 3 added the xcodeproj command-line tool for reading and editing targets, build phases, groups, build settings, xcconfig files, and Swift packages from the terminal. The beta 2 release also shipped xcprojformatter, which validates and rewrites project.xcproj files with sorted keys, and neither tool is on the PATH so both must be invoked via xcrun. The new format is JSON5, allowing trailing commas, comments, unquoted keys, and single-quoted strings, though the formatter converts everything except trailing commas back to plain JSON and drops comments. Xcode Projects for the Command Line and Coding Agents From the author iOS Daily One essential iOS development article a day, with a short summary. Free on Telegram. Learn More → https://t.me/+ltQId2u7xvQxYWYy Xcode 27.2 beta 2 introduced a new project format. Instead of the old project.pbxproj property list, a project can store its configuration in project.xcproj , a JSON file with readable keys and no object IDs for every build file. To switch, select the project in the Project navigator 1 , open the File inspector 2 , and choose JSON in the Project Format pop-up under Project Document 3 : Xcode also ships two command-line tools for the new format: - xcprojformatter came with the format in beta 2. It's a formatter for project files, like swift-format for code: it checks that a project.xcproj file is valid and rewrites it with sorted keys and consistent layout. - xcodeproj arrived in beta 3. It reads and edits projects from the terminal: targets, build phases, groups, build settings, xcconfig files, and Swift packages. Neither is on your PATH , so run them with xcrun . If you use CocoaPods, a plain xcodeproj in the terminal may run the xcodeproj Ruby gem instead. Check with which xcodeproj and always call Apple's tool as xcrun xcodeproj . Apple's article https://developer.apple.com/documentation/xcode/updating-your-xcode-project-configuration-file-format covers the format itself, and the tools come with detailed --help . This post covers what each tool does and where it helps in practice. Creating a project from scratch creating-a-project-from-scratch xcodeproj init creates an empty project in the JSON format: The trailing commas aren't a typo. The file is JSON5 https://json5.org , a superset of JSON that also allows comments, unquoted keys, and single-quoted strings. Xcode writes only the trailing commas. The formatter turns everything else back into plain JSON and drops comments. There's no template option, so everything Xcode's New Project sheet does takes a separate command. Here's the minimum for an iOS app with a single source file, Weather/WeatherApp.swift : What each step does: 1. Build phases. A new target has none, and nothing compiles without a Sources phase. The Frameworks phase is where package products get linked, and pkg add fails without it. Add a Resources phase too if the app has asset catalogs or other resources. 2. Synchronized group. Xcode picks up new files in the Weather folder automatically, so you never register them one by one. /Weather is a group path in the Project navigator, not a filesystem path, and every group and target command addresses groups this way. 3. Platform and Swift version. init doesn't add any defaults from Xcode's templates. Without SWIFT VERSION , the build fails. Without a deployment target, it defaults to the SDK version, so the app runs only on the latest OS. Set it to the oldest version you want to support. 4. Bundle ID and launch screen. Without a bundle ID, the app builds but simctl install rejects it. The launch screen key is what Xcode's app template sets. target add already sets PRODUCT NAME and GENERATE INFOPLIST FILE , so the app doesn't need an Info.plist file. Changing build settings changing-build-settings setting set writes to the project by default and to a target with --target . --config scopes a value to one configuration. Values that start with a dash need a bare -- before them, and -- ends option parsing, so put --config first: In the JSON file, both values end up in the same dictionary, with the configuration written as a suffix: get and list print raw values. They don't evaluate macros or read the contents of xcconfig files. For resolved values, use xcodebuild -showBuildSettings . The tool knows which settings are lists. OTHER LDFLAGS or GCC PREPROCESSOR DEFINITIONS go through setting set-multi , and setting set rejects them. The opposite also holds: OTHER SWIFT FLAGS is single-valued here, so set-multi rejects it. Moving settings to an xcconfig file moving-settings-to-an-xcconfig-file setting set takes one key and one value per call, so every setting is a separate command. To set several at once, put them in an xcconfig file: Then add it to the project and link it to every configuration: The project file only gets a reference, and the values stay in a file that's easy to read and review: A project-level xcconfig applies to every target, including test bundles you add later, so keep target-specific values like the bundle ID on the target itself. setting get doesn't read xcconfig files, so keys set there show up as unset: Adding Swift packages adding-swift-packages pkg add adds a remote package and, with --product and --target , links a product in the same step: --from means "up to next major version". The other requirement kinds from Xcode's UI have their own flags: | Requirement | Flag | |---|---| | Up to next minor version | --up-to-next-minor 1.2.0 | | Exact version | --exact 1.2.0 | | Range | --range-min 1.0.0 --range-max 2.0.0 | | Branch | --branch main | | Commit | --revision