Skip to content

init

ward init creates ward.toml using an interactive wizard or explicit options. It sets up Maven, Gradle, TypeScript, and JavaScript projects; see Node projects for what changes in a Node project.

Terminal window
ward init [-C <dir>] [--project <dir>]... [--preset <standard|strict>] [--integration <none|agent>[,...]] [--quality-hooks] [--remove-quality-hooks] [--ci github] [--agent-rules] [--gate <id>[,...]] [--tool openspec] [--openspec-validation <openspec|openspec-archived>[,...]] [--install-tool <tool|gate>[,...]] [--architecture <style>] [--overwrite] [--dry-run] [--accept-compatibility-updates] [--yes]
OptionWhat it does
-C, --directory <dir>Start in <dir> instead of the current directory. See Choosing projects.
--project <dir>Configure this Maven, Gradle, TypeScript, or JavaScript project, relative to -C. Repeat for several.
--preset <standard|strict>Pre-select the preset’s gates and rules. See Presets and gates.
--gate <id>[,...]Enable these gates. With --preset, adds to the preset’s gates.
--integration <none|agent>[,...]Install agent integrations. none cannot be combined with an agent.
--architecture <style>layered, hexagonal, or clean for the archunit and dependency-cruiser gates.
--tool openspecAdd OpenSpec as a project tool.
--openspec-validation <id>[,...]Add the openspec (active) or openspec-archived validation gate.
--install-tool <tool|gate>[,...]Consent to install a missing tool, such as trivy or openspec.
--quality-hooksAdd Ward checks to the Git pre-commit and pre-push hooks.
--remove-quality-hooksRemove only Ward’s sections from those hooks.
--ci githubWrite a GitHub Actions workflow that runs the ci profile.
--agent-rulesWrite the selected gates’ limits into a Ward-owned section of AGENTS.md.
--overwriteReplace an existing ward.toml without asking.
--dry-runPrint the change plan and exit without writing anything.
--accept-compatibility-updatesApprove validated Maven plugin version updates without asking.
--yesUse defaults for unanswered questions, skip the review, approve compatibility updates. See How questions are answered.
--verbosePrint initialization diagnostics to stderr. See Troubleshooting.

agent accepts the IDs documented for integrate, including codex, cursor, copilot, gemini, windsurf, cline, factory-droid, and amp.

  • standard and strict pre-select their offline gates. custom (wizard only) starts with none.
  • For TypeScript and JavaScript, see Node projects for what each preset selects.
  • The gates you confirm are exactly the gates configured. Unticking a pre-selected gate removes its ward.toml section, build declarations, and rule file. The preset’s rules still apply to the other gates.
  • With --preset, --gate only adds gates. It never removes preset gates.
  • Init does not run gates.

Some gates need extra input:

  • archunit asks for Layered, Hexagonal, or Clean architecture. If test sources already contain ArchUnit classes ending in Test or Tests, init runs those instead of generating a duplicate. Otherwise it creates a minimal ArchitectureTest.java; see Generated files.
  • dependency-cruiser in a TypeScript or JavaScript project asks for the same three styles. Init writes the config that enforces the style, unless the project has its own; see Generated files.
  • zap writes a <TARGET_URL> placeholder. Replace it with an authorized target before check will run the gate.
  • api-compat needs a released baseline. On Maven it is offered only when pom.xml declares <distributionManagement>; otherwise --gate api-compat fails with that explanation.
  • openapi-lint, openapi-diff, and openapi-compat are offered only when Git tracks an openapi.yml, openapi.yaml, or openapi.json in src/main/resources or the project root. Init writes that path as their spec; see OpenAPI contract gates.
  • dependency-check with trivy: init warns that trivy already scans dependencies without an API key, so you can usually drop one of them.

Init assigns each gate to cumulative profiles:

GatesProfiles
Fast: format, checkstyle, pmd, gitleaks, prettier, eslint, biomedefault, pre-commit, pre-push, ci
Slower local gates, including compile, tsc, the test runners, and custom commandspre-push, ci
Environment-dependent gatesci

default is the same fast tier as pre-commit, and ci contains every selected gate.

For format, enforcer, license, api-compat, and pmd, init adds missing plugins using Ward’s embedded Java compatibility catalog.

  • Existing compatible plugin versions are kept.
  • For an incompatible managed plugin, init shows the current version, the compatible version, the detected constraints, and the file, then asks for consent. Approve with --yes or --accept-compatibility-updates.
  • A declined update leaves the build file unchanged, and init reports that the gate was not reconfigured.

Ward supports Java 8 to 25. Init chooses every plugin and engine version for the JDK that runs your build, not for the JVM that runs Ward:

  • Maven: the Java version that mvn --version (or ./mvnw --version) reports.
  • Gradle: the Launcher JVM that gradle --version (or ./gradlew --version) reports. Init also reads the Gradle version, because some plugins need a newer Gradle.

The release the build targets (maven.compiler.release, sourceCompatibility, JavaLanguageVersion.of(n), or options.release) defaults to that JDK. A release above the runtime JDK cannot compile, so init stops and asks you to run the build on a newer JDK.

Each version was run on a real JDK in Ward’s compatibility matrix:

ToolJDK 8JDK 11JDK 17JDK 21JDK 25
Checkstyle engine9.310.20.210.20.210.20.210.20.2
SpotBugs (Maven plugin / Gradle engine)4.8.6.6 / 4.8.64.9.7.0 / 4.9.74.9.7.0 / 4.9.74.9.7.0 / 4.9.74.9.7.0 / 4.9.7
PIT1.17.41.30.11.30.11.30.11.30.1
Spotless (Maven / Gradle)2.30.0 / 6.13.02.46.1 / 7.2.13.10.3 / 8.10.43.10.3 / 8.10.43.10.3 / 8.10.4
google-java-format1.71.24.01.28.01.36.11.36.1

PMD (plugin 3.28.0, engine 7.17.0), JaCoCo 0.8.13, ArchUnit 1.5.1, and Enforcer 3.5.0 use the same version on every JDK.

Every strict gate runs on each LTS JDK from 8 to 25. When no version of a gate’s tool runs on your JDK, init leaves that gate out and configures the others. The plan, --dry-run, and the init output name it:

format omitted: spotless-maven-plugin needs JDK 8+; this project builds on JDK 7
  • A gate you pass with --gate that cannot run on your JDK stops init before it changes any file.
  • In the interactive gate list, such a gate is shown as unavailable and cannot be selected.
  • The Gradle dependency-check and sonar plugins need JDK 11, so on Gradle they are omitted on JDK 8.
  • Above Java 25, init configures the project exactly as for JDK 25 and warns that Ward has not been tested on that JDK and does not support it.
  • Below Java 8, init warns that Ward supports Java 8 to 25 and may not work with that version.
  • Neither warning stops init.
  • Plugins go only into the project-level <build><plugins>, and dependencies only into the project-level <dependencies>. Missing sections are created.
  • Profiles, dependencyManagement, and plugin dependencies are never edited. A plugin declared only inside a profile is reported as not reconfigured.
  • The one addition to <build><pluginManagement> is the strict enforcer pins described in First failures under strict. Existing entries there never change.
  • Compatibility updates change only the managed plugin’s own project-level <version>.
  • New declarations match the file’s indentation. The rest of pom.xml stays byte for byte as it was.
  • mutation adds pitest-maven 1.30.1 with pitest-junit5-plugin 1.2.3, which read Java 25 class files.
  • The maven-pmd-plugin declaration enables <analysisCache>true</analysisCache>. On Ward’s own sources this cut a warm PMD run from about 8 s to 2 s.

Before writing, Ward checks the edited pom.xml in-process, without running Maven or fetching anything. The file must be well-formed XML, and each added declaration may contain only elements Maven allows there. If a check fails, pom.xml is not written, the error names the tool, and init rolls back its other changes.

Neither build tool gets PIT history settings: since PIT 1.30, incremental analysis needs the commercial Arcmutate history plugin.

  • ward.toml includes partial tables for checkstyle, pmd, format, semgrep, gitleaks, eslint, prettier, and biome. The check after each agent edit, and the pre-commit hook, skip gates the change cannot affect and narrow these tools to the edited or staged files. Other gates, ward check without --path, the pre-push hook, and CI still check the whole project. gitleaks scans those files directly, so it sees changes that are not yet committed; its full run scans the Git history.

  • ArchitectureTest.java goes in the package shared by all main sources, for example com.acme.shop for com.acme.shop.domain and com.acme.shop.application. Its rules analyze that whole package. The layer rule passes while layer packages are empty, so a new project does not fail before they exist.

  • The gitleaks gate writes a redacted SARIF report, so ward check can attach findings (file, line, and rule) to a failed gate:

    [quality-gates.gitleaks]
    command = ["gitleaks", "detect", "--no-banner", "--redact",
    "--report-format", "sarif", "--report-path", "target/gitleaks.sarif"]

    Gradle projects use build/gitleaks.sarif. Ward creates the report directory before the gate runs, so it works on a fresh clone. Configurations from earlier versions keep working without findings; re-run ward init or add the four report arguments to enable them.

  • The license plugin matches license names exactly and case-sensitively, so init adds licenseMerges that map common spellings to one name. The policy depends on the preset:

    • standard denies GPL and AGPL, mapping spellings such as GPL-3.0 and GNU Affero General Public License v3.0 to the excluded names. Licenses it does not recognize pass, so unusual GPL spellings are not caught.
    • strict allows only Apache-2.0, MIT, BSD-2-Clause, BSD-3-Clause, ISC, EPL-1.0, EPL-2.0, and CC0-1.0. Any other license, or a dependency without one, fails the build. A dual-licensed dependency passes when one of its licenses is allowed. LGPL, MPL, and CDDL are not listed because they need a legal decision; add an approved license to includedLicenses and its spellings to licenseMerges.
  • On Gradle, the license gate runs checkLicense from the com.github.jk1.dependency-license-report plugin, and init writes a licenseReport {} block whose rules are case-insensitive regular expressions. The plugin reads licenses from POMs and from files inside the jar, and a dependency passes when any of them matches a rule.

    • standard allows every license except GPL and AGPL. LGPL, the Classpath Exception, and dependencies without a license pass. Other GPL exceptions, such as MySQL’s Universal FOSS Exception, only cover open-source use, so they fail.
    • strict allows only Apache-2.0, MIT, BSD, ISC, EPL, and CC0 or public domain. Any other license, or a dependency without one, fails.
    • Earlier versions configured com.github.hierynomus.license, whose licenseMain checks source headers, not dependencies. Re-run ward init to switch; remove the old plugin yourself if you do not use it for headers.
  • An existing ward.toml is kept unless you confirm the overwrite or pass --overwrite.
  • Preset rule files are Ward-owned. Unmarked existing rule files are kept and reported.

The generated rule files load as they are, so a gate that fails right after init reports a real finding. With strict, expect these on most existing projects:

  • checkstyle: findings such as MagicNumber, MissingJavadocMethod, or MethodLength in existing code.
  • enforcer: strict keeps requirePluginVersions on purpose, so every plugin needs a pinned version.
    • Init pins the plugins Maven adds through its default lifecycle (clean, resources, compiler, surefire, jar, install, deploy, and site) in <pluginManagement>. It uses the versions Maven 3.9.16 binds by default and skips plugins the POM already declares. The pins appear in --dry-run.
    • The rule can still fail on other plugins, such as maven-war-plugin, or when a parent POM supplies a version. Pin those in <pluginManagement> with the versions the message reports.
    • Use standard if you do not want this rule.
  • license: “There are some forbidden licenses used” (Maven) or “Some libraries do not declare at least one allowed license” (Gradle) for any dependency whose only licenses are not on the allowlist, for example an LGPL-only JDBC driver. On Maven, add the license to includedLicenses once your legal policy approves it, or add its spelling to licenseMerges when it is an approved license written another way. On Gradle, add a rule to licenseReport.

Init sets up TypeScript and JavaScript projects with npm, pnpm, Yarn, or Bun. Every Node tool runs as a local dev dependency through the package manager’s runner: npx, pnpm exec, yarn, or bunx.

  • A project is TypeScript when tsconfig.json sits next to package.json, or a workspace member has one.
  • A TypeScript project’s framework comes from the packages it declares in dependencies or devDependencies, never from file names: vue or nuxt is Vue, svelte or @sveltejs/kit is Svelte, astro is Astro, and @angular/core is Angular. If several match, the first in that order (Astro, Svelte, Vue, Angular) decides.
  • For Vue, Svelte and Astro, the type-check gate is the framework’s checker in place of tsc: vue-tsc (vue-tsc --build), svelte-check, and astro-check (astro check). tsc is not offered for those projects.
  • For Angular, the type-check gate is ngc -p tsconfig.app.json --noEmit, which checks component code and templates. The test gate is ng-test: ng test --watch=false --coverage, run with the project’s own test builder. Ward writes no test config for Angular and edits no angular.json, karma.conf.js or tsconfig*.json. Angular’s default vitest dependency does not select the vitest gate; a declared jest keeps jest.
  • Angular’s coverage minimums are enforced only when angular.json sets coverageThresholds. When it sets none, the plan says so and names angular.json and karma.conf.js as the files to set them in.
  • Angular’s ESLint config uses angular-eslint, which lints .html templates. Init pins angular-eslint to the release for the project’s Angular major (21.4.0 or 22.6.0) and checks the Node range of that release.
  • stryker is not offered for Angular, and the strict preset omits it, because no Stryker runner works with Angular’s test builder. The plan says so.
  • biome is refused for Angular, because Biome does not check templates. Init stops with an error that names biome and suggests eslint, and changes no file.
  • A framework project whose TypeScript configuration has no inputs fails the type-check gate and names the file. vue-tsc --build reports TS18002 for a root tsconfig.json with "files": [] and no references, and TS18003 for a referenced config with "include": []. A root with references to configs that have sources passes.
  • Stryker mutates only the src folder in Vue, Svelte and Astro projects, so component files and tests are not mutated. Other projects keep Stryker’s default.
  • The generated ESLint config ignores each framework’s output folder: .astro/ for Astro and .svelte-kit/ for Svelte.
  • Biome and framework lint rules are out of scope: a Biome project with a framework keeps Biome’s current behavior, and eslint lints .vue, .svelte, or .astro files only when the project’s own ESLint config handles them.
  • Any other package.json is JavaScript, and counts as a project only when it holds .js, .jsx, .mjs, or .cjs sources of its own. Init does not count files in node_modules, build output (dist, build, out, target, coverage), hidden directories, directories with another build file (such as pom.xml), packages that are not workspace members, or root tool configs such as vite.config.js. A package without sources, such as a monorepo root, is skipped; see Choosing projects.
GateCommand (npm)Profiles
tscnpx tsc --noEmit --pretty falsepre-push, ci
vitestnpx vitest run --coveragepre-push, ci
jestnpx jest --coveragepre-push, ci
bun-testbun test --coveragepre-push, ci
strykernpx stryker runci
prettiernpx prettier --check .default, pre-commit, pre-push, ci
eslintnpx eslint . --format jsondefault, pre-commit, pre-push, ci
biomenpx biome ci --colors=off --reporter=checkstyle . (--error-on-warnings under strict)default, pre-commit, pre-push, ci
dependency-cruisernpx depcruise . --config <file> --output-type errpre-push, ci
semgrepsemgrep scan --config p/typescript --error (p/javascript for JavaScript)ci
  • JavaScript gets the same gates except tsc.
  • A project that uses Biome gets the biome gate instead of prettier; see Biome.
  • The test runner is the one the project declares: vitest, then jest. A Bun project without either gets bun-test, and any other project gets vitest with @vitest/coverage-v8.
  • semgrep stays opt-in, and gitleaks and trivy are offered as for Java.
standardstrict
Gatestsc (TypeScript only), the test runner, prettier and eslint (or biome in a Biome project)the same, plus stryker and dependency-cruiser
Coverageat least 70% of linesat least 85% of lines and 75% of branches (Bun: lines only, because it does not measure branches)
ESLint (generated config)recommended rulesTypeScript: typescript-eslint strict-type-checked; JavaScript: recommended rules
Mutation scorenoneat least 60%, in ci

A custom selection uses the standard coverage minimum, and stryker without strict reports its score without a minimum. dependency-cruiser is offered by both presets, and strict selects it.

When package.json already declares eslint or @biomejs/biome, init keeps that version, as long as it is inside Ward’s supported range. Ward tests the oldest and newest version of each range on Node 22 and 24, with the TypeScript fixture.

ToolWard pinsSupported rangeTested versions
eslint10.12.0>=9.22.0 <11.0.09.22.0, 10.13.0
@biomejs/biome2.5.15>=2.3.0 <3.0.02.3.0, 2.6.3

A declared version outside its range is an INCOMPATIBLE finding that names the tool and the range. --dry-run lists it, and applying the plan fails without changing any file. Pick a version inside the range, or skip the gate.

Init detects Biome when package.json declares @biomejs/biome or the project root has its own biome.json, biome.jsonc, .biome.json, or .biome.jsonc. It asks no question about Biome.

  • A Biome project gets one biome gate, which lints and checks formatting in one run. It replaces prettier and, unless the project has its own ESLint config, eslint. With strict, warnings fail the gate too (--error-on-warnings).
  • A project’s own ESLint config (eslint.config.* or .eslintrc*) stays, and its eslint gate runs that config unchanged. Init writes no ESLint or Prettier config for a Biome project, and a Prettier config the project has stays in place without a gate.
  • Under strict, a project’s own biome.json or biome.jsonc is layered (see Layered configs). A .biome.json or .biome.jsonc is kept with a warning. Under standard, a project’s own Biome config is kept unchanged and listed as user-owned, with a warning that the preset’s Biome rules are not applied to it. Otherwise init writes an ownership-marked biome.jsonc with Biome’s recommended rules. It also turns on Git-ignore support, so Biome skips .ward/reports/ and coverage output. strict sets these rules to error: noExplicitAny, noNonNullAssertion, useConst, noUnusedImports, noUnusedVariables, noExcessiveCognitiveComplexity, noFloatingPromises, and noMisusedPromises.
  • If the project does not declare @biomejs/biome, init adds it at 2.5.15. A declared version outside >=2.3.0 <3.0.0 is an INCOMPATIBLE finding, and applying changes nothing. Biome 2.3.0 is the first release with the checkstyle reporter that Ward reads, and 3.x may change the config schema.
  • Edit hooks run Biome on the edited files only. An edited file that Biome ignores passes.

The thresholds sit in the tools’ own config files, so you can see and tune them there:

  • When the project has no test config, init writes a Ward-owned vitest.config.mjs or jest.config.mjs with the preset’s coverage thresholds, and the test command carries no threshold flag.
  • When the project has its own config (vitest.config.* or vite.config.* for Vitest; jest.config.* or a "jest" key in package.json for Jest), init keeps it unchanged and lists it as user-owned. The command then passes the thresholds, for example npx vitest run --coverage --coverage.thresholds.lines=85 --coverage.thresholds.branches=75.
  • Bun reads its threshold from bunfig.toml, and Stryker reads its minimum from stryker.config.mjs.
  • Projects set up by an earlier Ward version keep the threshold flag on their test command. Running init again moves it into vitest.config.mjs or jest.config.mjs.

Under strict, a TypeScript project’s own flat ESLint config (eslint.config.*), or the project’s own biome.json or biome.jsonc, is not replaced. Init moves it to a base file and writes Ward’s root config in its place:

Project fileBase fileWard’s root
eslint.config.mjseslint.base.mjseslint.config.mjs
eslint.config.js with "type": "module"eslint.base.jseslint.config.js
eslint.config.js without "type"eslint.base.jseslint.config.mjs
eslint.config.cjseslint.base.cjseslint.config.mjs
eslint.config.tseslint.base.tseslint.config.ts
biome.jsonbiome.base.jsonbiome.jsonc
biome.jsoncbiome.base.jsoncbiome.jsonc
  • ESLint: Ward’s root imports the base file and appends the strict rules, so Ward’s rules win over the base’s. The base’s own rules still apply, so a framework’s rules, such as Next’s core-web-vitals or Nest’s Jest globals, keep working. The base file is ignored by the lint.
  • Biome: Ward’s biome.jsonc extends the base file and sets the eight strict rules itself, so they win over the base’s settings. Biome checks the same files it checked before.
  • The base file’s content is not changed. Editors, npx eslint, npm run lint, and ward check all read the combined config.
  • Running ward init --preset strict again regenerates Ward’s root and leaves the base file alone.
  • If the base file name is already taken by a file Ward did not write, init stops with a conflict and changes no file. Rename or move that file, then run init again.
  • To undo the layering, delete Ward’s root config and rename the base file back to the project’s own name. The project’s config is then the user’s again.
  • Init adds each dev dependency the selected gates need and the project does not declare, with an exact version from Ward’s catalog and no range prefix. A package already in dependencies or devDependencies keeps its version, and existing tool configs are reused.
  • The edit inserts entries into devDependencies, or creates it, with the file’s own indentation; nothing else in the file changes. A file whose layout cannot be located, such as a minified one, fails init before anything is written.
  • Then init runs <manager> install from the project root. When nothing is missing, no install runs. --dry-run lists the packages and the install without running it.
  • If the install fails, init fails naming the command, restores package.json and the lock file byte for byte, removes a lock file the install created, and writes no ward.toml. A later init failure rolls back the same way. node_modules is not restored; the next install converges.
PackageVersionNode
eslint10.12.0^20.19.0 || ^22.13.0 || >=24
@eslint/js10.0.1^20.19.0 || ^22.13.0 || >=24
typescript-eslint8.71.1^18.18.0 || ^20.9.0 || >=21.1.0, TypeScript >=4.8.4 <6.1.0
prettier3.9.10>=14
@biomejs/biome2.5.15>=14.21.3 (declared versions must be >=2.3.0 <3.0.0)
vitest, @vitest/coverage-v84.0.18^20.0.0 || ^22.0.0 || >=24.0.0
typescript6.0.3>=14.17
globals17.13.0>=18
@stryker-mutator/core, @stryker-mutator/vitest-runner, @stryker-mutator/jest-runner10.1.1>=22.0.0
dependency-cruiser18.6.0^22.0.0 || ^24.0.0 || >=26.0.0, TypeScript >=2.0.0 <7.0.0

The eslint gate adds eslint, @eslint/js, and typescript-eslint (plus typescript when it is missing) to a TypeScript project, and eslint, @eslint/js, and globals to a JavaScript project, with no TypeScript package. The stryker gate adds @stryker-mutator/core and the runner plugin for Vitest or Jest. Bun has no runner plugin, so Stryker runs bun test through its command runner. Stryker 10 needs Node 22 or later. The dependency-cruiser gate adds dependency-cruiser, and typescript to a TypeScript project when it is missing.

Before writing, init checks each package it would add against the project’s Node version (from .nvmrc, .node-version, or engines.node, else 24), and typescript-eslint against the declared typescript version (the lower bound of its range, or 6.0.3 when init adds it).

  • A package outside its range is an INCOMPATIBLE finding naming the package and the range. --dry-run lists it, and applying fails without changing any file. A Node or TypeScript version cannot be fixed by choosing another package version, so there is no approval option.
  • For example, "typescript": "7.0.2" with the eslint gate fails because typescript-eslint 8.71.1 supports TypeScript >=4.8.4 <6.1.0. Pin TypeScript 6.0 or lower, or skip eslint.
  • Node is compared on its major version. When a range needs a later minor, such as ^22.13.0 for ESLint on Node 22, the plan notes the minimum version.
  • Ward supports Node 22 and 24. For a project that declares an older Node version, init shows a warning that Ward may not work with it. The warning does not stop init or change the files it writes, and Bun projects do not get it.
  • eslint.config.js (with "type": "module") or eslint.config.mjs (otherwise) when the eslint gate is selected and the project has no eslint.config.* or .eslintrc*. It is a flat config ignoring dist/, coverage/, .ward/, .next/, out/, and build/ (framework build output), and it starts with a // managed-by: ward marker. Under strict, a TypeScript project’s own flat config is layered instead (see Layered configs). Otherwise an existing config is reused unchanged and listed as user-owned, and the plan warns that the preset’s ESLint rules are not applied to it. A project with no tests fails the test gate until it adds one, because a test gate that passes with no tests is misleading.

    • For TypeScript it enables the @eslint/js and typescript-eslint recommended rules. With strict it enables strict-type-checked instead, with type information from tsconfig.json through the project service. Type-checked rules are off for .js, .mjs, and .cjs files, such as init’s own configs, because they sit outside tsconfig.json. TypeScript files that tsconfig.json does not include, such as excluded tests, fail with a project-service error: add them to tsconfig.json. Under strict, empty classes that carry a decorator, such as a NestJS module, are allowed.
    • For JavaScript it enables the @eslint/js recommended rules and declares both browser and Node globals from globals, so require, process, and window pass no-undef. It parses JSX in .jsx files. Without "type": "module" it also parses .js files as CommonJS, because ESLint parses them as ES modules otherwise. With the jest gate it also declares Jest’s globals (test, expect, describe, …) for files under __tests__ and *.test.* or *.spec.* files. Narrow the globals to your runtime once the first check passes.
  • vitest.config.mjs or jest.config.mjs with the coverage thresholds, unless the project has its own test config; see Where the thresholds live.

  • bunfig.toml for bun-test when none exists, with [test] coverageThreshold = { lines = 0.7 } (0.85 with strict) and a # marker. An existing bunfig.toml is kept, and init warns that the coverage threshold is enforced only if it sets one.

  • stryker.config.mjs for stryker when the project has no stryker.conf.*, stryker.config.*, or .stryker.conf.*. It names the test runner, enables the json, clear-text, and progress reporters, and sets thresholds with high: 80 and low: 60. With strict it also sets break: 60, so Stryker fails below a 60% mutation score. An existing Stryker config is kept with its own thresholds.

  • .dependency-cruiser.mjs for dependency-cruiser when the project has no dependency-cruiser config of its own (.dependency-cruiser.{json,js,cjs,mjs,ts,cts,mts}). In the chosen style, a module in one layer must not import another. A layer is a directory with that name in the module’s path, and the config does not cruise node_modules:

    StyleModules in a path namedMust not import modules in
    layeredcontrollerrepository
    hexagonaldomain, applicationadapter, infrastructure
    cleandomain, corecontroller, adapter, infrastructure

    strict also forbids circular dependencies. In a TypeScript project, type-only imports count, and the config names tsconfig.json when it exists. A layer folder that does not exist matches nothing, so the gate passes. A config the project wrote is used as it is. Init rewrites Ward’s file when you change the style or preset, and its first line is // managed-by: ward with no preset.

  • These generated configs are ES modules (.mjs) in every project and start with a // managed-by: ward preset=<id> marker. Init rewrites them when you change the preset, and never touches a config you wrote.

  • biome.jsonc for the biome gate when the project has no Biome config of its own. It starts with a // managed-by: ward preset=<id> marker; see Biome.

  • .gitignore gets a Ward-owned section with .ward/reports/ and coverage/, created when the file does not exist. With stryker it also lists .stryker-tmp/ and reports/mutation/. Prettier and Biome skip Git-ignored files, so they do not check these reports.

  • ward.toml gets partial tables for eslint and prettier: an edit hook runs them on the edited TypeScript and JavaScript files only, with a command that drops the full run’s .. tsc and the tests have none, so they run in full in pre-push and ci.

Init installs a missing tool only with your explicit consent, one tool at a time.

ToolHow
Trivy, GitleaksHomebrew
SemgrepHomebrew, or pipx as a fallback
ZAP (zap-baseline.py)Manual guidance only
OpenSpec--install-tool openspec
  • In the wizard, each unavailable tool gets a Yes/No question. No is the default, and blank input or end of input declines.
  • In non-interactive runs, consent with --install-tool <gate>, for example --install-tool trivy.
  • --yes never installs tools and never bypasses unresolved prerequisites. --dry-run lists the install commands but runs none. ward check never installs gate tools.

When trivy runs after a selected Maven or Gradle gate, init adds --offline-scan, so Trivy resolves dependencies from the local repository instead of Maven Central (why).

Quality hooks are opt-in and separate from agent integrations. --quality-hooks adds Ward-managed sections to .git/hooks/pre-commit and .git/hooks/pre-push:

  • Each section goes after the shebang and before your own logic. It does not replace the hook.
  • pre-commit runs ward check --profile pre-commit --format json with each staged file as a --path, so partial checks cover the commit’s files; pre-push runs ward check --profile pre-push --format json on the whole project.
  • Re-run the option to refresh the sections.
  • --remove-quality-hooks removes only Ward’s marked sections. It keeps your content, executable permissions, and other Ward integrations.
  • A malformed or duplicate marker is reported as a conflict and left unchanged.

Without --ci, init does not generate a CI workflow. --ci github writes .github/workflows/ward.yml, which runs ward check --profile ci on pull requests and pushes to main. The workflow:

  • installs the Ward release that ran init (WARD_VERSION);
  • for a Java project, sets up the JDK that runs your build (JDK 25 when init cannot read it), since the pinned tools were chosen for that JDK;
  • pins the tools the selected gates need (gitleaks, trivy, semgrep, OpenSpec);
  • fetches full history for gitleaks and caches the trivy database;
  • passes the NVD_API_KEY secret to dependency-check;
  • uploads the SARIF report to code scanning for pull requests from the same repository.

In a Node project the workflow sets up Node or Bun instead of Java and installs the dependencies from the lock file first; see CI for Node projects.

The file is Ward-owned, so re-running init refreshes it. An existing workflow at that path without Ward’s marker blocks initialization as a conflict. Gates that need a running application, such as zap, are not set up in the workflow.

--agent-rules writes a Ward-owned section into AGENTS.md with the limits the selected gates enforce: Checkstyle limits, PMD rule sets, SpotBugs effort, coverage and mutation minimums, and enforcer rules.

  • For TypeScript it names the tsc, Biome (or ESLint and Prettier), and test commands, the 70% line coverage minimum (85% of lines and 75% of branches with strict), Stryker (with its 60% minimum under strict), and the ESLint config in use, and no Java tool.

  • For JavaScript it names the same without tsc.

  • Content outside the section’s markers is kept.

  • When a rule file is user-owned, the section points to the file instead of quoting preset values.

  • Init changes AGENTS.md only when you pass --agent-rules, and then rewrites the section from the preset. Pass it again after tuning rules only if you want the preset values back.

Init configures the Maven, Gradle, TypeScript, or JavaScript project that contains the -C directory. Started inside a build module, it configures the aggregator that lists the module, so ward.toml lands next to the aggregator’s pom.xml or settings.gradle(.kts).

When the -C directory is not inside a project init supports, such as the root of a pnpm or Turbo monorepo whose package.json has no JavaScript sources of its own, init looks for projects below it:

  • It skips hidden directories and node_modules, target, build, dist, and out.
  • Modules count toward their aggregator instead of being listed.
  • With one project found, init configures it and says so.
  • With several, the wizard asks which to configure, all checked. Without a terminal, or with --yes, init stops and lists the --project values to pass.
  • With none, init stops without changing any file. If the -C directory is inside a project of another build tool, such as Poetry or Cargo, the error names that build tool and says that init supports Maven, Gradle, TypeScript and JavaScript projects. Inside a JavaScript package without sources, it reports that no supported project was detected.
  • .js files inside a Java project, such as src/main/resources/static/app.js, belong to that project, so they do not make the repository root a JavaScript project.
  • A root with a stray script of its own, such as scripts/release.js, is a JavaScript project and stops discovery. Pass --project to choose the projects below it instead.

--project <dir> names a project explicitly, relative to -C. Repeat it to configure several projects in one run:

Terminal window
ward init --project apps/api --project services/billing --preset standard --integration none

Each value must be a Maven, Gradle, TypeScript, or JavaScript project root. A module or a subdirectory fails before anything is planned, and the error names the project to use instead. The answers apply to every selected project; run init again with a single --project to give one project different settings.

Ward keeps two roots apart:

  • The project root holds ward.toml, the build file, the rule files, and AGENTS.md.
  • The repository root, the Git top-level, holds the files that Git, GitHub, and agent sessions read only there.
FileSingle-project repositoryProject in apps/api
Quality hooks.git/hooks/pre-commit, pre-pushthe same files, with a section keyed by apps/api
CI workflow (--ci github).github/workflows/ward.yml.github/workflows/ward-apps-api.yml
Claude Code.claude/settings.json, .claude/hooks/ward-edit-writeroot .claude/settings.json, .claude/hooks/ward-edit-write-apps-api
OpenCode.opencode/...root .opencode/..., files suffixed -apps-api
Other agents (Codex, Gemini, Copilot, …).codex/hooks.json, …the same root files, shared by every project
OpenSpec gatesopenspec/ in the projectroot = "../.." when openspec/ exists only at the repository root

The plan shows repository-level files relative to the project root, such as modify ../../.git/hooks/pre-commit. A run that configures several projects lists them once under Repository root. A project whose root is the repository root gets exactly the files it always did.

The other agents read a single hook file at the repository root, so every project shares one installation:

  • The hook names no project. Ward checks the project whose ward.toml is closest above each edited file, and at turn end every configured project with changes.
  • Edits outside every configured project, such as apps/web/page.tsx, run nothing.
  • Removing such an integration removes it for every project in the repository.
  • Each question starts with Step N of M. The total can grow when an answer adds a follow-up question, such as the architecture style after you select archunit.
  • Press Enter to accept the default shown in brackets: Enter=1 (standard) at the preset question and Enter=layered at the architecture question.
  • Gates whose tool is not installed are marked [unavailable]. If you select one anyway, init configures it and prints how to make it available, for example Selected quality gate 'semgrep': UNAVAILABLE - ....
  • The arrow-key menus redraw in place, so your earlier answers stay on screen.
WhereHow to answer
macOS and Linux terminalsUp/Down to move, Space to toggle, Enter to confirm. q, Q, or Ctrl+C cancels.
Windows, piped input, or no raw terminal modeNumbered prompts. Pre-selected items show as [x]; each number you enter toggles that item, and an empty line accepts the pre-selection.

For example, after choosing standard, entering the number of pmd configures every standard gate except pmd.

Run ward init again and answer y to the overwrite question, or pass --overwrite.

  • The preset question defaults to the preset recorded in ward.toml (custom when none is recorded).
  • The gate list starts with the currently enabled gates, so changing one gate takes a couple of key presses.
  • Integrations, hooks, tools, and the architecture style are asked as usual.
  • If the existing file can’t be read, the usual defaults apply.

Answering n keeps the file and prints Kept existing ward.toml. No changes were made.

See Tools init can install.

Before writing anything, init works out exactly which files it would change. On a terminal it shows that plan and asks Proceed? [Y/n]. Press Enter or answer y to apply it. Answer n, or end the input, and nothing changes. The review is skipped with --yes and when input is not a terminal.

Terminal window
Ward will make these changes:
create ward.toml
modify pom.xml
+ maven-pmd-plugin 3.28.0
~ spotless-maven-plugin 1.0.0 -> 3.10.3 (requires approval)
create config/pmd-ruleset.xml
keep config/checkstyle.xml (user-owned, not changed)
create .git/hooks/pre-commit
create .claude/settings.json
Proceed? [Y/n]:
Plan entryMeaning
create, modify, removeEvery file init would change, including agent-integration files and Git hooks.
+ plugin versionA plugin init would add to pom.xml or the Gradle build file.
~ plugin old -> newA managed plugin version update; (requires approval) when it still needs approval.
keepA user-owned file init leaves alone.
conflictA user-owned file that blocks initialization. Init stops with exit code 2 before writing anything.

--dry-run resolves your answers the same way, prints the plan, and exits with code 0. It writes no files, installs nothing (including OpenSpec), and asks for no compatibility approval:

Terminal window
ward init --dry-run --preset strict --integration none --architecture layered

Each question is answered by the first of these that applies:

  1. The option that answers it, such as --preset, --gate, --integration, --architecture, --tool, or --quality-hooks. --ci and --agent-rules are options only; the wizard does not ask for them.
  2. Its default, when --yes is given: preset standard with its gates, integration none, no tools, OpenSpec validations, or installation, no quality hooks, and architecture layered. --yes never overwrites an existing ward.toml (use --overwrite) and never installs OpenSpec (use --install-tool openspec).
  3. An answer read from input.

On a terminal, options never hide the questions they don’t answer, so ward init --integration claude-code still asks for the preset, gates, tools, and quality hooks.

Without a terminal, for example in CI:

  • When --preset, --gate, or --integration is given, init reads nothing. Unanswered questions take their defaults, so an open but silent standard input can’t make it wait.
  • Without options, answers can be piped, for example printf '1\n1\n\n' | ward init.
  • The preset and gate selection is the only required answer. If input ends before it, or it is missing in a non-interactive run, init exits with code 2 and suggests --preset, --gate, or --yes.
  • If input ends at any other question, init uses its default and continues. The exception is the OpenSpec installation prompt, where initialization is cancelled and no files change.
  • On a terminal, an invalid answer (such as 4 at the preset question, an out-of-range number, or an empty gate selection) shows the error and asks again, keeping your earlier answers. The raw-key menus stay open after an empty confirmation.
  • With piped input, an invalid answer fails immediately with exit code 2 and no changes.

The wizard has a separate optional Tools screen. OpenSpec and its two validations are unselected by default:

  • active: openspec validate --all --strict --no-interactive
  • archived: openspec validate --archived --no-interactive

Selecting a tool or validation is not consent to install it. --install-tool openspec is explicit consent; without it, Ward only writes the selected validations and reports manual remediation.

Add --verbose to the root command or to init to route initialization decisions to stderr. The diagnostics identify ownership conflicts, applied changes, and rollback outcomes without printing file contents:

Terminal window
ward --verbose init
ward init --verbose
CodeMeaning
0Initialization succeeded, or --dry-run printed the plan.
2Invalid usage, or init could not initialize: a conflict, a cancelled or invalid selection, or a missing required answer.