{"slug": "xcode-projects-for-the-command-line-and-coding-agents", "title": "Xcode Projects for the Command Line and Coding Agents", "summary": "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.", "body_md": "# Xcode Projects for the Command Line and Coding Agents\n\nFrom the author\n\n### iOS Daily\n\nOne essential iOS development article a day, with a short summary. Free on Telegram.\n\n[Learn More →](https://t.me/+ltQId2u7xvQxYWYy)\n\nXcode 27.2 beta 2 introduced a new project format. Instead of the old\n`project.pbxproj` property list, a project can store its configuration in\n`project.xcproj`, a JSON file with readable keys and no object IDs for every\nbuild file. To switch, select the project in the Project navigator (1), open\nthe File inspector (2), and choose **JSON** in the **Project Format** pop-up\nunder **Project Document** (3):\n\nXcode also ships two command-line tools for the new format:\n\n- `xcprojformatter` came with the format in beta 2. It's a formatter for\nproject files, like`swift-format` for code: it checks that a`project.xcproj` file is valid and rewrites it with sorted keys and\nconsistent layout.\n- `xcodeproj` arrived in beta 3. It reads and edits projects from the\nterminal: targets, build phases, groups, build settings, xcconfig files,\nand Swift packages.\n\nNeither is on your `PATH`, so run them with `xcrun`.\n\nIf you use CocoaPods, a plain `xcodeproj` in the terminal may run the\n`xcodeproj` Ruby gem instead. Check with `which xcodeproj` and always call\nApple's tool as `xcrun xcodeproj`.\n\nApple's\n[article](https://developer.apple.com/documentation/xcode/updating-your-xcode-project-configuration-file-format)\ncovers the format itself, and the tools come with detailed `--help`. This\npost covers what each tool does and where it helps in practice.\n\n## [Creating a project from scratch](#creating-a-project-from-scratch)\n\n`xcodeproj init` creates an empty project in the JSON format:\n\nThe trailing commas aren't a typo. The file is\n[JSON5](https://json5.org), a superset of JSON that also allows comments,\nunquoted keys, and single-quoted strings. Xcode writes only the trailing\ncommas. The formatter turns everything else back into plain JSON and drops\ncomments.\n\nThere's no template option, so everything Xcode's New Project sheet does\ntakes a separate command. Here's the minimum for an iOS app with a single\nsource file, `Weather/WeatherApp.swift`:\n\nWhat each step does:\n\n1. **Build phases.** A new target has none, and nothing compiles without a\nSources phase. The Frameworks phase is where package products get linked,\nand`pkg add` fails without it. Add a Resources phase too if the app has\nasset catalogs or other resources.\n2. **Synchronized group.** Xcode picks up new files in the`Weather` folder\nautomatically, so you never register them one by one.`/Weather` is a\ngroup path in the Project navigator, not a filesystem path, and every`group` and`target` command addresses groups this way.\n3. **Platform and Swift version.**`init` doesn't add any defaults from\nXcode's templates. Without`SWIFT_VERSION` , the build fails. Without a\ndeployment target, it defaults to the SDK version, so the app runs only\non the latest OS. Set it to the oldest version you want to support.\n4. **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\ntemplate sets.\n\n`target add` already sets `PRODUCT_NAME` and `GENERATE_INFOPLIST_FILE`, so the\napp doesn't need an `Info.plist` file.\n\n## [Changing build settings](#changing-build-settings)\n\n`setting set` writes to the project by default and to a target with\n`--target`. `--config` scopes a value to one configuration. Values that start\nwith a dash need a bare `--` before them, and `--` ends option parsing, so put\n`--config` first:\n\nIn the JSON file, both values end up in the same dictionary, with the configuration written as a suffix:\n\n`get` and `list` print raw values. They don't evaluate macros or read the\ncontents of xcconfig files. For resolved values, use\n`xcodebuild -showBuildSettings`.\n\nThe tool knows which settings are lists. `OTHER_LDFLAGS` or\n`GCC_PREPROCESSOR_DEFINITIONS` go through `setting set-multi`, and `setting set`\nrejects them. The opposite also holds: `OTHER_SWIFT_FLAGS` is single-valued\nhere, so `set-multi` rejects it.\n\n### [Moving settings to an xcconfig file](#moving-settings-to-an-xcconfig-file)\n\n`setting set` takes one key and one value per call, so every setting is a\nseparate command. To set several at once, put them in an xcconfig file:\n\nThen add it to the project and link it to every configuration:\n\nThe project file only gets a reference, and the values stay in a file that's easy to read and review:\n\nA 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.\n\n`setting get` doesn't read xcconfig files, so keys set there show up as\nunset:\n\n## [Adding Swift packages](#adding-swift-packages)\n\n`pkg add` adds a remote package and, with `--product` and `--target`, links a\nproduct in the same step:\n\n`--from` means \"up to next major version\". The other requirement kinds from\nXcode's UI have their own flags:\n\n| Requirement | Flag | \n|---|---|\n| Up to next minor version | `--up-to-next-minor 1.2.0` | \n| Exact version | `--exact 1.2.0` | \n| Range | `--range-min 1.0.0 --range-max 2.0.0` | \n| Branch | `--branch main` | \n| Commit | `--revision <sha>` | \n\nThe rest of the `pkg` commands cover what's left in the Package Dependencies\ntab:\n\n- `pkg add-local` adds a local package.\n- `pkg link --embed` links a product and embeds a dynamic framework.\n- `pkg link-plugin` runs a build-tool plugin.\n- `pkg unlink` ,`pkg unlink-plugin` , and`pkg remove` undo them.\n\n## [Checking the result](#checking-the-result)\n\nA few read-only commands show what the project contains:\n\nPhases are addressed by their index from `target info`, not by kind, because\na target can have several phases of the same kind.\n\nThe whole project file is now about 60 lines, and `xcodebuild` builds it as\nis. `xcodeproj` never creates schemes or workspaces, but `xcodebuild`\ngenerates a scheme automatically:\n\n## [Old projects and smaller diffs](#old-projects-and-smaller-diffs)\n\n`xcodeproj` isn't limited to the new format. It edits `project.pbxproj` files\ntoo, and the changes are minimal:\n\n`xcodeproj` can't convert a project to JSON, but `xcodebuild` can. It's the\nsame switch as the File inspector, without opening Xcode:\n\nThe new file replaces the old one, so commit before converting.\n\nProjects in the JSON format also open in Xcode 27.0 and 27.1, so the whole team doesn't have to be on the beta. The command-line tools ship only with 27.2.\n\nThe new format pays off in diffs. Here's the same `pkg add` of\n`swift-collections` in both formats. The property list gets 24 new lines in\nfive places, three generated IDs, and three new sections:\n\nThe JSON file gets 12 lines in two places, and a reviewer can read them:\n\n## [Catching broken files with xcprojformatter](#catching-broken-files-with-xcprojformatter)\n\n`xcprojformatter` parses a project file and prints it back in canonical form:\nkeys sorted, defaults omitted, comments removed. It doesn't support\n`project.pbxproj`.\n\nAn invalid file makes it exit with an error. Here's what it prints for a few broken edits:\n\n| Edit | Error | \n|---|---|\n| Remove a target's `id` | Missing required value for key “id” at “/targets[0]” | \n| Misspell the `compile-sources` phase | Unexpected value “compile-source” | \n| Set a target's `name` to a number | Expected an instance of string but an instance integer was specified | \n\nOnly the first error includes a path to the problem. The other two don't say where it is.\n\nUnknown keys don't cause an error. The formatter silently drops them. A typo\nlike `build-setings` produces a valid project that ignores your settings. The\nformatter shows it as a removed line:\n\nSo the useful check is whether the formatter changes anything. That makes a good CI step:\n\nIt exits with an error when someone edits the file by hand without formatting it, and the diff shows exactly which lines the formatter would drop.\n\n## [Resolving merge conflicts in JSON](#resolving-merge-conflicts-in-json)\n\nThe JSON format doesn't make conflicts disappear. I added a package and a build setting on one branch, and a different package, a different setting, and a widget target on another. The new target merged cleanly, but both package additions and both settings conflicted, since sorted entries land next to each other:\n\nThese conflicts are readable, but don't just keep both sides. Here it creates\nan object with two `repository` keys. The formatter accepts it and silently\ndrops one, so the merged project loses a package without an error:\n\nArray entries have to become separate objects again:\n\nDictionary entries like build settings are fine to keep side by side as long\nas the keys differ. After resolving, the formatter diff may move lines into\nsorted order, but it shouldn't drop any. Run\n`xcrun xcprojformatter --update`, then check that `xcrun xcodeproj pkg list`\nand `xcrun xcodeproj target list` show both branches' changes.\n\nOr skip the manual merge: take one side of the file and replay the other\nbranch's changes with `xcodeproj`. Changes that merged cleanly, like the widget\ntarget here, need replaying too.\n\n## [Handing the CLI to agents](#handing-the-cli-to-agents)\n\nYou'll rarely type the commands above yourself. Adding a target in Xcode is\nstill faster than five commands and a few build settings. The CLI is for\ncoding agents: Apple pitches the JSON format as easier for them to edit, and\n`xcodeproj` looks designed for them too. Its help pages are long, explain each\nedge case, and point to the next command. Errors list what's available or\nsay which command to use instead:\n\nAgents can edit the JSON file directly, but the tool is safer. It generates unique IDs for new targets and products, keeps the format canonical, and checks things like dependency cycles before it writes anything.\n\nAn agent won't reach for the tool on its own, though. It's new, it's not on\nthe `PATH`, and models haven't seen it in training data. Everything this post\ncovers, from the order of commands to the gotchas below, is context the agent\nneeds. A few lines in `CLAUDE.md` or `AGENTS.md` pass it along:\n\nThere's no project name in it, so you can paste it into any project. With one\n`.xcodeproj` in the folder, `xcodeproj` finds it automatically. With several,\nthe agent passes `--project`, or you set `XCODEPROJ_PROJECT` to the project's\npath.\n\n### [What about Xcode's MCP server?](#what-about-xcodes-mcp-server)\n\nXcode also gives agents an MCP server through `xcrun mcpbridge`, and some of\nits tools overlap with `xcodeproj`. `XcodeNewProject` and\n`XcodeNewTarget` create projects and targets from Xcode's templates,\n`XcodeMakeDir` creates synchronized folders, and `GetTargetBuildSettings` and\n`UpdateTargetBuildSetting` read and change target build settings.\n\nThe MCP tools go through a running Xcode with the project open, or through\nheadless Xcode, which you turn on with `sudo xcrun mcp-server enable`. I\ncovered that setup in\n[Headless Xcode: From Prompt to Simulator with MCP](https://artemnovichkov.com/blog/headless-xcode-from-prompt-to-simulator-with-mcp).\n\nThe MCP tools can't add Swift packages, link xcconfig files, or edit build\nphases, and they change build settings only per target, without project-level\nor per-configuration values. If you already use the MCP server, it covers\ntemplates, previews, and running the app on a simulator. `xcodeproj` handles\nthe project file edits the MCP tools can't do, and it's a plain command with\nno server or permissions to set up.\n\n## [Gotchas](#gotchas)\n\nA few things I ran into while testing.\n\n### [Add build phases before synchronized groups](#add-build-phases-before-synchronized-groups)\n\nIf you include a filesystem-synchronized group before the target has a\nSources phase, `xcodeproj` puts every file in the folder into the target's\nexclusions:\n\nNothing compiles, and the build fails at linking with no hint about why. Including the group again doesn't help, since it's already included. Include each excluded file instead:\n\nOr add the phases first.\n\n### [Test targets need manual wiring](#test-targets-need-manual-wiring)\n\nXcode's New Target sheet does more than `target add` does. A unit test target\ncreated from the CLI has no host app, so `@testable import` fails. Here's the\nfull setup, assuming a `WeatherTests` folder with the tests:\n\nThe last line is needed for projects created with `init`. Projects from\nXcode's templates already have it.\n\n`xcodeproj` also never touches schemes. If your project has a shared scheme,\nits Test action won't include the new target, and\n`xcodebuild test -scheme Weather` won't run the new tests. `xcodebuild`\ngenerates a separate `WeatherTests` scheme, so either test with that scheme or\nadd the target to the shared one in Xcode. `xcodebuild -list` doesn't show\nthe `WeatherTests` scheme, but `-scheme WeatherTests` still works.\n\n### [Keep xcconfig files out of synchronized groups](#keep-xcconfig-files-out-of-synchronized-groups)\n\nPutting `Project.xcconfig` next to the sources in `Weather/` seems natural,\nbut `config set` refuses to link it:\n\nThe error is longer than that and suggests the fix: keep xcconfig files in a\nregular group, like `Config` above, outside any synchronized folder.\n\n## [Going further with the Swift package](#going-further-with-the-swift-package)\n\nApple also open-sourced the format as\n[xcode-project-format](https://github.com/apple/xcode-project-format), a\nSwift package that models `project.xcproj` as value types under the\n`XCSchema` namespace. `xcprojformatter` is built on it. If the CLI doesn't\ncover your case, for example a custom linter that checks every target's\ndeployment target, you can read and write the project with typed models\ninstead of parsing JSON:\n\nThe format and both tools are still in beta and have rough edges, as the\ngotchas above show. [Tuist](https://tuist.dev) and\n[XcodeGen](https://github.com/yonaskolb/XcodeGen) remain an option, but for\nmany projects a readable project file and a CLI from Apple may be enough.", "url": "https://wpnews.pro/news/xcode-projects-for-the-command-line-and-coding-agents", "canonical_source": "https://artemnovichkov.com/blog/xcode-projects-for-the-command-line-and-coding-agents", "published_at": "2026-10-15 00:00:00+00:00", "updated_at": "2026-10-15 16:56:26.190698+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Apple", "Xcode", "Xcode 27.2", "project.xcproj", "project.pbxproj", "xcprojformatter", "xcodeproj", "CocoaPods"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/xcode-projects-for-the-command-line-and-coding-agents", "markdown": "https://wpnews.pro/news/xcode-projects-for-the-command-line-and-coding-agents.md", "text": "https://wpnews.pro/news/xcode-projects-for-the-command-line-and-coding-agents.txt", "jsonld": "https://wpnews.pro/news/xcode-projects-for-the-command-line-and-coding-agents.jsonld"}}