Running the connector in Claude
The connector is a local MCP server over stdio. Claude Code and Claude Desktop both start it as a child process; it talks to the local bank over HTTP and HTTPS and to its key helper over stdio.
Before Claude: the bank and the build
npm install
bank/scripts/pf-up.sh # PingFederate with the modules (6-8 minutes the first time)
node bank/scripts/federation-up.mjs enrol # the Trust Anchor, and PingFederate + the Trust Mark Issuer as its subordinates
bank/scripts/services.sh start # attester, identity provider, Trust Mark Issuer, bank API, PDP, partner
npm run build # dist/connector.mjs
bank/scripts/services.sh status shows what is up. npm run e2e proves the whole path with software keys before a person is involved.
Keys
| Backend | Set up | Chosen when |
|---|---|---|
YubiKey (yubikey-piv) |
python3 -m venv helpers/yk-signer/.venv && helpers/yk-signer/.venv/bin/pip install yubikey-manager==5.9.2, then once, by the YubiKey's owner: change the factory PIN and PUK (helpers/yk-signer/.venv/bin/ykman piv access change-pin, change-puk) and protect the management key (helpers/yk-signer/.venv/bin/ykman piv access change-management-key --generate --protect). ykman lives in the venv, not on your PATH |
a set-up YubiKey is plugged in, or backend: "yubikey-piv" |
Secure Enclave (secure-enclave) |
make -C helpers/se-signer bundle (Xcode, team JH6RX4DRG2; Node.js's official build at NODE_BINARY, default the nvm 20.19.3 one). On macOS 27 the bank gets App Attest evidence; on 26 and earlier it records the keys as self-asserted |
no set-up YubiKey is plugged in, or backend: "secure-enclave" |
| Software | tests only: CONNECTOR_ALLOW_SOFTWARE_KEYS=1 and CONNECTOR_KEY_BACKEND=software |
never by default |
Once an agent has keys, it stays with that backend until bank_reset.
The YubiKey helper keeps its keys in PIV slots 82, 83 and 84 and will not replace a key it did not create. It asks for the PIN in its own macOS dialog; never type a PIN into Claude.
Claude Code
.mcp.json at the root of this repository registers the connector for this project:
{
"mcpServers": {
"claude-bank-connector": {
"command": "node",
"args": ["dist/connector.mjs"],
"env": { "FEDCONN_INSECURE_LOCALHOST": "1" }
}
}
}
Start claude in the repository, approve the project's MCP server when asked, and /mcp lists its nine tools.
Claude Desktop
Claude Desktop starts MCP servers with a minimal environment, so every path is absolute. In ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"claude-bank-connector": {
"command": "/opt/homebrew/bin/node",
"args": ["/Users/davidhyland/Source/idp-fed-agentic-connector/dist/connector.mjs"],
"env": { "FEDCONN_INSECURE_LOCALHOST": "1" }
}
}
}
Restart Claude Desktop. Use command -v node for the first path if Node lives elsewhere.
Run the connector in one Claude app at a time: both share the same state directory and the loopback port.
The demo
Before starting: in the browser, PingFederate's page at https://localhost:9131 carries a self-signed certificate, so the browser warns once - in Chrome, Advanced, then Proceed to localhost. The passkey you create is saved for localhost in whatever you choose (iCloud Keychain with Touch ID, or a security key); delete it afterwards if you like. The test customer's password is in the rig:
grep TF_VAR_test_user_password ~/Source/pf-agentic-identity-connector/conformance/secrets.env
- "Enrol my bank agent." Claude calls
bank_enroland a browser tab opens at the bank's sign-in page. Create a passkey (any username), then approve Touch ID when the Secure Enclave helper asks - that is the agent's own key proving itself. Claude pollsbank_statusuntil the stage isenrolled. - "Let it read my accounts."
bank_authorizeopens PingFederate: sign in assuite-userand approve. Touch ID again, at most once every five minutes. - "What are my balances and recent transactions?"
bank_get_accountsandbank_get_transactions; each answer says who the token names as the customer (sub) and which agent acted (act). - "Any offers from the bank's partners?" The partner is a separate mandate:
bank_authorizewithaudience: "partner", approve, thenpartner_get_offers. The mandate states its purpose - personalised benefits - and the answer shows how the partner decided: its enforcement point, configured by the federation, asked the federation's layer and then its own PDP, and each returns its checks. - "How does the federation see you?"
bank_explain_trustshows the agent's mission three ways - what it declares, what the bank vouches for and caps, and what every member resolves - then resolves its chain to the Trust Anchor. - "Could you use those offers for marketing?" Ask Claude to get a partner mandate for
DirectMarketing. The bank's PDP refuses it before any consent page (purpose_outside_missioninbank/.run/pdp.log). TryAccountManagementand the bank refuses again - within the agent's mission, but not a purpose the partner accepts (purpose_not_accepted).
The finale is the bank withdrawing its trust. Take the agent's entity id from bank_status, then, as the bank's operator:
curl -s -X POST http://localhost:7400/admin/revoke -H "authorization: Bearer $(cat bank/.secrets/authority-admin.token)" -H 'content-type: application/json' -d '{"sub":"<entity id>"}'
Ask for the partner's offers again: the federation's layer refuses (trust_mark_inactive), while the bank's own API still answers. Then revoke the agent itself:
curl -sk -X DELETE "<entity id>" -H "authorization: Bearer $(cat bank/.secrets/authority-admin.token)"
Now the partner's layer cannot resolve the agent at all (not_in_federation), and the bank's authorization server refuses its next request with invalid_client. bank_reset (with confirm: true) and a fresh bank_enrol start again.
Keep PingFederate running for the whole demo: its registry of hosted agents is in memory, so a restart forgets the agent.
Settings
All optional. Defaults point at this repository's local bank.
| Variable | Default | What it does |
|---|---|---|
CONNECTOR_KEY_BACKEND |
auto |
secure-enclave, yubikey-piv, or software (tests) |
CONNECTOR_STATE_DIR |
~/Library/Application Support/idp-fed-agentic-connector |
the agent's state, 0600 files |
CONNECTOR_BROWSER |
open |
none only reports the URL (tests, headless) |
CONNECTOR_INSTANCE_GATE |
presence |
Secure Enclave inst key: presence, biometry or none |
CONNECTOR_INSTANCE_PROTECTION |
when-unlocked |
or after-first-unlock |
CONNECTOR_SCOPE_BANK, CONNECTOR_SCOPE_PARTNER |
accounts:read transactions:read, offers:read |
what each mandate asks for |
FEDCONN_INSECURE_LOCALHOST |
off | 1 skips certificate checks for loopback hosts only - PingFederate's self-signed listener. FEDCONN_CA_FILE=<pem> trusts a specific certificate instead, and is better |
FEDCONN_HOME |
found from the bundle's location | where helpers/ lives, if the bundle is moved |
BANK_ISSUER, BANK_ENROLMENT_URL, IDP_ISSUER, BANK_API, PARTNER_RS, TRUST_ANCHOR |
the local bank | point the connector at another bank |
When something goes wrong
The extension, and other people's Macs
make -C helpers/se-signer mcpb packs the signed app as a Claude Desktop extension, build/ClaudeBankConnector.mcpb,
pointed at the Railway demo's bank: double-click it and Claude Desktop installs it. Claude Desktop runs the
extension's server/index.js with its own Node.js; that starts the app's own signed node on the sealed
connector.mjs - the only caller the helper accepts - with NODE_OPTIONS stripped, and registers the app with
LaunchServices so App Attest answers. The demo site offers the file at /download/ClaudeBankConnector.mcpb and
explains it at /connect.
Signed with the team's Apple Development certificate, the build runs on Macs registered to the team and nowhere
else. make -C helpers/se-signer release makes the one for anyone's Mac - a Release build, sealed with the team's
Developer ID with the hardened runtime and no entitlements (App Attest answers without any; the Xcode build's
get-task-allow is what notarisation refuses), notarised, stapled, packed - once two things the account holder
has to make are on this Mac:
A Developer ID Application certificate for team
JH6RX4DRG2, in the login keychain. Xcode → Settings → Accounts → the team → Manage Certificates → + → Developer ID Application (only the Account Holder can create one); or developer.apple.com → Certificates → + → Developer ID Application, then open the downloaded.cer.security find-identity -v -p codesigningshould then list it; the Makefile picks it up by itself.Notarisation credentials, stored once as a keychain profile named
fedconn-notary, with your own Apple ID and an app-specific password from appleid.apple.com (or an App Store Connect API key):xcrun notarytool store-credentials fedconn-notary --apple-id you@example.com --team-id JH6RX4DRG2It prompts for the app-specific password and keeps it in the keychain; nothing lands in the repo.
Then make -C helpers/se-signer release and deploy/railway/deploy.sh demo to publish the new file. Node.js's
binary keeps its own Developer ID signature (team HX7739G8FX, hardened runtime), which is what notarisation
wants of nested code and what the helper checks its caller against. Untested until the certificate exists:
whether App Attest answers under a Developer ID signature; it does under a development one with the hardened
runtime and no entitlements, which is the same shape.
Without a Mac at all, the same connector is served to Claude.ai, ChatGPT and Claude Code from the demo site:
https://idp-fedconn-demo.up.railway.app/mcp, with its keys on the server (the demo page's connector, over MCP).
| Symptom | Cause | Fix |
|---|---|---|
E_LOCKED |
the Mac is locked and the Secure Enclave inst key is when-unlocked |
unlock the Mac |
E_SETUP_REQUIRED from the YubiKey |
its PIV management key is not PIN-protected | the one-time management-key command above, if you are happy to change it |
E_KEY_EXISTS |
PIV slot 82, 83 or 84 holds a key this agent did not create - often its own keys from before a bank_reset |
on firmware 5.7 or later, helpers/yk-signer/.venv/bin/ykman piv keys delete <slot>; older YubiKeys cannot delete PIV keys, so enrol again with replace_yubikey_keys: true once you are sure nothing else uses those slots |
appAttest.supported: false |
macOS 26 or earlier (App Attest arrived in macOS 27), or a copy of the app LaunchServices has not registered | upgrade, or lsregister -f the app (make bundle does); on macOS 26 the bank attests the key as self-asserted |
E_CALLER_UNVERIFIED |
the helper inside ClaudeBankConnector.app was asked by something other than the app's own node running its sealed connector.mjs, or the app's seal is broken |
run the connector as the app's Contents/MacOS/node Contents/Resources/connector.mjs, with no NODE_OPTIONS; rebuild the app if it was edited |
invalid_client ... revoked at this authority |
the bank revoked this agent | bank_reset, then enrol again |
| partner refuses after a PingFederate restart | the hosted-entity registry is in memory, so the agent's federation entity is gone | enrol again (a durable registry fixes this) |
| the loopback URL fails | port 53682 is taken, often by the other Claude app running the connector | run it in one app at a time |