Anthropic's code-modernization playbook says: understand the code first, rank the risk, define the tests before any code changes, then port one component at a time while old and new run side by side. Every one of those steps needs a legacy system an agent can read, run and compare against, freely and repeatedly, without touching production. This page shows each phase as the exact MCP calls a team makes on SteelFrame, and gives you a project kit for Claude Code that does it. SteelFrame is CobolStack's clean-room z/OS-compatible engine, aligned with the practices in the playbook; it is not Anthropic's product and not IBM z/OS, and nothing here implies endorsement by either.
the four phases as calls the kit worked example: CardDemo POSTTRAN rules the kit enforces governance costs MCP docs & live tool reference back to the terminal
test/playbook_kit_battle.mjs): a fresh user registers, mints a Developer key, runs the
characterization against a scratch CardDemo estate, runs it again (byte-identical: the clock is pinned to
the hundredth), and the Python tests pass on the fresh fixture. The same path was walked by hand on the
hosted estate before this page went up.
Tool names are the live catalog's (/api/mcp/tools); every call is
tools/call over POST /mcp with a bearer API key. Reads are free. Two doors matter:
/mcp (the persistent lane, for reading the estate) and /mcp?lane=ephemeral (the
RAM-only lane, for everything that runs — inside a workspace that is a copy of the estate and that vanishes
when you destroy it).
{"name":"find_program","arguments":{"name":"CBTRN02C"}}
# → source, transitive copybooks, load modules, the JCL steps that run it, CSD slice,
# mapsets, the scheduler jobs, a summary (LOC, paragraphs, CALLs, every SELECT + DD + key)
{"name":"read_member","arguments":{"dsn":"CARDDEMO.SOURCE","member":"CBTRN02C"}}
{"name":"read_member","arguments":{"dsn":"CARDDEMO.JCL","member":"POSTTRAN"}}
{"name":"decode_records","arguments":{"dsn":"AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS",
"copybook":"CARDDEMO.COPYLIB(CVACT01Y)","count":"5","numbers":"decimal"}}
# → rows beside the byte truth; charset stated (PS = EBCDIC at rest, KSDS = program image); the field layout
{"name":"analysis_graph","arguments":{"kind":"job"}} # who reads / writes which dataset
{"name":"analysis_impact","arguments":{"field":"ACCT-ID","program":"CBTRN02C"}} # field-level data flow
{"name":"analysis_metrics","arguments":{"program":"CBTRN02C"}} # cyclomatic complexity, dead paragraphs
Output of the phase: a discovery note per component that cites the calls it came from (the kit's CBTRN02C.md is one).
{"name":"inventory_scan","arguments":{}} # programs, jobs, screens, copybooks, flags, refs
{"name":"analysis_metrics","arguments":{}} # every program's metrics row + estate-dead copybook fields
{"name":"analysis_graph","arguments":{"kind":"call"}} # fan-in / fan-out
Output: a risk register ranked by coupling through shared data, complexity, dead code, third-party dependencies and whether the component can be characterized. The playbook's rule — "start with a single component or workflow that has clear boundaries and moderate complexity" — becomes a row you can point at. POSTTRAN qualifies: batch-only, six files, no DB2/CICS, 619 lines.
{"name":"create_workspace","arguments":{"seed":"clone","name":"posttran-d002","ttlSeconds":3600,"jobNumber":1000}}
# → {"workspace":"W3E6C87","status":"RUNNING", ...} poll get_workspace while CREATING; "workspace":"W3E6C87" on every call below
{"name":"run_batch","arguments":{"workspace":"W3E6C87","records":"none","spool":"tail","lines":[
"//PBKPREP JOB 'PLAYBOOK PREPARE',CLASS=A,MSGCLASS=0,NOTIFY=&SYSUID",
"//STEP05 EXEC PGM=IDCAMS","//SYSPRINT DD SYSOUT=*","//SYSIN DD *",
" DEFINE CLUSTER (NAME(MYUSER.CARDDEMO.TRANSACT.KSDS) CYLINDERS(1 5) KEYS(16 0) -",
" RECORDSIZE(350 350) SHAREOPTIONS(2 3) INDEXED)",
" DEFINE GENERATIONDATAGROUP (NAME(MYUSER.CARDDEMO.DALYREJS) LIMIT(5) SCRATCH)",
"/*"]}}
# what discovery found: CBTRN02C opens the transaction master OUTPUT, and on a shared estate the installed
# cluster is UPDATE-only for ordinary users and may already be loaded without REUSE (FILE STATUS 37, as on
# z/OS); CardDemo's DEFGDGB never defines the DALYREJS base the deck writes. So the files the job WRITES
# go under your own HLQ (the kit writes {{USER}}); the masters it reads and REWRITEs stay the estate's — in
# the workspace copy.
{"name":"load_dataset","arguments":{"workspace":"W3E6C87","dsn":"MYUSER.CARDDEMO.DALYTRAN.PS",
"copybook":"CARDDEMO.COPYLIB(CVTRA06Y)","mode":"replace",
"allocate":{"attrs":{"dsorg":"PS","recfm":"FB","lrecl":350}},"rows":[ ...seven transactions you chose... ]}}
{"name":"run_batch","arguments":{"workspace":"W3E6C87",
"lines":[ ...CARDDEMO.JCL(POSTTRAN) with TRANFILE, DALYTRAN and DALYREJS(+1) pointed at MYUSER.CARDDEMO.*... ],
"before":true,"records":"both","clock":{"fixed":"2022-07-18 10:20:30"},
"copybooks":{"MYUSER.CARDDEMO.TRANSACT.KSDS":"CARDDEMO.COPYLIB(CVTRA05Y)",
"MYUSER.CARDDEMO.DALYTRAN.PS":"CARDDEMO.COPYLIB(CVTRA06Y)",
"AWS.M2.CARDDEMO.ACCTDATA.VSAM.KSDS":"CARDDEMO.COPYLIB(CVACT01Y)",
"AWS.M2.CARDDEMO.TCATBALF.VSAM.KSDS":"CARDDEMO.COPYLIB(CVTRA01Y)",
"AWS.M2.CARDDEMO.CARDXREF.VSAM.KSDS":"CARDDEMO.COPYLIB(CVACT03Y)"}}}
# → steelframe-run/1: maxcc 4, STEP15=4, joblog{STEP15.SYSOUT: 6 DISPLAY lines},
# outputs{ TRANFILE 4 rows, ACCTFILE 50 rows, TCATBALF rows, DALYREJS.G0001V00 3 records (hex), ... },
# before{ the same datasets as they were } — TRAN-PROC-TS is 2022-07-18-10.20.30.000000 on every run
{"name":"destroy_workspace","arguments":{"id":"W3E6C87"}}
The kit's tools/characterize.mjs makes these calls from one JSON spec and writes
fixtures/<name>/fixture.json (steelframe-fixture/1: return codes, the SYSOUT
lines, every file keyed by its DD name with before/after rows — or bytes where the estate has no copybook —
and its role: input, updated, output). That file is the expected value
of every test in phase 4; nothing in it came from reading the COBOL. A failed run (abend, or a return code
above the spec's limit) is still written but reported as RUN FAILED with the joblog tail and
exit code 3 — a fact, not a baseline.
FIXTURE= and fail for the right reason before the port exists.tools/fixture-diff.mjs
compares two captures (oracle vs re-capture, oracle vs your system's output written in the fixture shape) and
answers identical / different / missing per artifact with the first differing row. For the legacy
side alone the engine also has run_capture → POST /api/captures/{id}/compare./mcp/vscode
(vscode_python_write / vscode_python_test, Java, .NET, Node, Go, …) inside your own
container; locally, any toolchain — the fixture is JSON.Download playbook-kit.tgz MIT; browse the files at /playbook-kit/README.md · CLAUDE.md · .mcp.json · .claude/settings.json · skills/sf-characterize · tools/characterize.mjs
| file | what it is for |
|---|---|
| CLAUDE.md | the standing rules for Claude Code on this estate: which server for which phase, workspaces, pinned clocks, costs, facts-not-verdicts, never the live estate, secrets, numbers, charsets |
| .mcp.json | two HTTP servers: steelframe → /mcp (reads) and steelframe-private → /mcp?lane=ephemeral (every run); the key is ${SF_TOKEN} from your environment, expanded by Claude Code, never in a file |
| .claude/settings.json | a permissions.deny list of the 53 persistent-lane tools that change the estate, submit work or run a session — Claude Code enforces it and removes them from the agent's context; the engine's battle keeps the list equal to the live catalog's non-read-only tools |
| .claude/skills/sf-* | /sf-discover, /sf-risk-rank, /sf-characterize, /sf-port, /sf-compare — one procedure per phase, with the calls above and what to write down |
| tools/sfmcp.mjs | one MCP call from a shell or CI, zero dependencies (Node 18+): node tools/sfmcp.mjs --lane ephemeral run_batch @call.json |
| tools/characterize.mjs | the phase-3 chain from a JSON spec → fixtures/<name>/fixture.json; prints every step and what it cost; destroys the workspace |
| tools/fixture.mjs · fixture-diff.mjs | the steelframe-fixture/1 format (from a steelframe-run/1 record) and the comparison with verdicts per artifact |
| carddemo-posttran/ | the worked example below |
# ten minutes to a passing characterization test
curl -sO https://steelframe.cobolstack.com/playbook-kit.tgz && tar -xzf playbook-kit.tgz && cd playbook-kit
export SF_TOKEN=sf_... # a Developer-preset key from https://learn.cobolstack.com/account/#apikeys (shown once)
node tools/characterize.mjs carddemo-posttran/posttran.characterize.json --out carddemo-posttran/fixtures
cd carddemo-posttran/python && python3 -m unittest -v test_posttran # 7 tests, stdlib only
claude # then: /sf-discover CBTRN02C
| phase | artifact in the kit | what it holds |
|---|---|---|
| 1 Discovery | discovery/CBTRN02C.md | purpose, where it runs (POSTTRAN STEP15, CA-7 folder CARDDEMO), the six files with DD → DSN → organisation/key → copybook → charset, the record layouts, the business rules quoted from the source (validation order: 103 overrides 102), coupling through ACCTDATA/TCATBALF/TRANSACT with the CICS side, the prerequisites a run needs, open questions for the SME |
| 2 Risk | (one row in the note) | batch-only, no DB2/CICS/third-party, 619 LOC, 27 paragraphs, coupling through data — a first candidate |
| 3 Tests first | posttran.characterize.json → fixtures/posttran-d002/fixture.json | clock 2022-07-18 10:20:30; the POSTTRAN deck with the three files it writes under your HLQ and a prepare step that defines the empty transaction master and the DALYREJS GDG base there; seven seeded daily transactions covering every path (update-existing and create-new category balances, a refund, an unknown card, over-limit, after-expiry); the fixture: maxcc 4, six SYSOUT lines, TRANFILE 4 rows, ACCTFILE and TCATBALF before/after, DALYREJS 3 records as EBCDIC bytes (no copybook on the estate), about 120 KB |
| 4a Port | python/posttran.py · test_posttran.py | a literal Python port (paragraph names as comments, decimals, two documented deviations: size-error truncation and a duplicate TRAN-ID raise instead of silently differing); seven tests that load the fixture — RC, SYSOUT, transactions written, account master after, category balances after, the reject file byte for byte, the roles the oracle reported |
| 4b Compare | tools/fixture-diff.mjs | a second capture is identical (the pinned clock pins the hundredths too; files compare by DD name, so two users' captures compare); on an estate someone has been using, the reference masters differ and the diff says exactly where, while the tests still pass against your own capture |
/mcp door — create_workspace there needs the administrator scope
workspace.write and answers 403 KEY_SCOPE. It can on /mcp?lane=ephemeral
(scope ephemeral.exec, in the preset): a RAM-only copy of the estate, the same tools, nothing
retained. The kit routes every run there and denies the persistent door's changing tools in
.claude/settings.json, so "an agent never changes the live estate" is enforced by Claude Code,
not asked of the model.
clock:{fixed} on run_batch / cics_open_session /
cics_run_script (dates, times and, since this release, the hundredths of FUNCTION CURRENT-DATE
and ACCEPT FROM TIME); jobNumber on the workspace so JOBIDs line up. JES timestamps in
JESMSGLG/JESYSMSG are real: the fixture leaves those files out.fixture-diff are
your side; when they disagree with the port, the fixture is not edited.CREATING, destroy when done; on 429 wait —
the defaults are 2 workspaces per user and 2 RAM sandboxes per plane.| control | on SteelFrame today |
|---|---|
| identity | every call runs as the key's user id under RACF-style security; keys are self-service, shown once, stored hashed, with a name, last-used and an activity trail (identity is the user, not yet the agent instance) |
| least privilege | per-tool scopes; Read-only / Developer / Full / Custom presets clamped to the user's own rights; administrator scopes never on a self-service key; the kit's deny list on top |
| credentials | keys expire (default 90 days), rotate and revoke immediately, take an IP allow-list, a daily spend cap, an MCP-only or REST-only surface; static bearer keys (OAuth with short-lived tokens is on the list) |
| audit | every MCP tool call that reaches its route is logged with the user, door, tool and status — free reads included, never the content; usage by key with CSV export |
| sandboxing | the RAM-only lane with zero retention; workspaces that are a child engine on a copy of the estate; VS Code seats in containers with a read-only root, no capabilities and no internet |
| approval gates | destructive and open-world hints on the tools; Git pushes and writes to a connected real mainframe wait for the user's click on the page; the screen door's Stop button |
The full, honest table — including what is not there yet — is in the blog post Claude's Code Modernization Playbook, in Plain Words — and How to Run It End to End on SteelFrame.
SteelFrame credit: reads, lists, status and analysis free; writes, compiles and SQL $0.03 per call; a run $0.03 + $0.99 per core-hour after the first 10 s of CPU; workspaces unmetered; failed and refused calls free; an identical retry within 60 s free. The worked example's capture is one prepare job, one seed and one run — about $0.09; a new account's signup credit covers it. Every billable result ends with its cost and your balance; each key has a rolling 24-hour cap. Details and the live tool reference: /mcp-docs.html.
SteelFrame and SteelFrame X are independent clean-room implementations by CobolStack, not affiliated with, endorsed by or sponsored by IBM or Anthropic. Anthropic, Claude and Claude Code are trademarks of Anthropic, PBC. AWS CardDemo is Amazon's sample application (Apache-2.0). The playbook kit is MIT-licensed; copy it into your own repository.