The modernization playbook on SteelFrame

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.

Proven, not promised. The kit's worked example is run by the engine's own battle (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.

The four phases, as calls

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).

1 · Discovery — code archaeology free reads, /mcp

{"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).

2 · Risk analysis — isolated candidates first free reads, /mcp

{"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.

3 · Tests before code — characterize on a workspace /mcp?lane=ephemeral

create_workspace→run_batch (prepare)→load_dataset (seed)→ run_batch {before, records, clock}→fixture.json→destroy_workspace
{"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.

4 · Incremental implementation with continuous validation your toolchain / a seat

  1. Tests first: one test per observable in the fixture (RC, SYSOUT, each updated/output dataset, the roles); they load the fixture through FIXTURE= and fail for the right reason before the port exists.
  2. Port one component, literally: paragraph names kept as comments, validation order kept, money in decimals, deviations raised instead of hidden (the kit's posttran.py).
  3. Compare on your side: the tests are the compare for the port; 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.
  4. In a SteelFrame seat, the same loop runs through /mcp/vscode (vscode_python_write / vscode_python_test, Java, .NET, Node, Go, …) inside your own container; locally, any toolchain — the fixture is JSON.

The kit

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

filewhat it is for
CLAUDE.mdthe 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.jsontwo 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.jsona 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.mjsone MCP call from a shell or CI, zero dependencies (Node 18+): node tools/sfmcp.mjs --lane ephemeral run_batch @call.json
tools/characterize.mjsthe 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.mjsthe 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

Worked example — CardDemo POSTTRAN (CBTRN02C), end to end

phaseartifact in the kitwhat it holds
1 Discoverydiscovery/CBTRN02C.mdpurpose, 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 firstposttran.characterize.json → fixtures/posttran-d002/fixture.jsonclock 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 Portpython/posttran.py · test_posttran.pya 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 Comparetools/fixture-diff.mjsa 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

Rules the kit enforces, and why

Which door, honestly. A self-service key (Developer preset) cannot create a workspace on the persistent /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.

Governance (what the zero-trust guide asks for, as it stands)

controlon SteelFrame today
identityevery 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 privilegeper-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
credentialskeys 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)
auditevery 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
sandboxingthe 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 gatesdestructive 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.

Costs

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.