$ npx rulesync-cli pull✓ Wrote CLAUDE.md (2 rulesets)# Coding Standards- Always use async/await- Prefer named exports
Rule Writing

AI Rules for Perl and Legacy Code

AI generates Perl 4-era patterns and makes legacy code worse. Rules for modern Perl, safe maintenance, and incremental modernization of legacy systems.

7 min read·November 27, 2025

AI generates Perl 4 patterns in 2026 — modern Perl deserves modern rules

use strict, Moo OOP, safe legacy maintenance, and incremental modernization

Why Perl and Legacy Code Need Special AI Rules

Perl occupies a unique niche in the AI coding rules landscape: most Perl codebases are legacy systems that have been running for decades. The challenge isn't writing new Perl — it's safely maintaining and incrementally modernizing existing Perl without breaking what works. AI assistants make this harder by generating ancient Perl patterns (Perl 4 syntax from the 1990s) and by making changes to legacy code without understanding the hidden dependencies and implicit behaviors.

The dual challenge: first, AI generates bad Perl (no strict, no warnings, bareword filehandles, format statements, indirect object syntax). Second, AI makes unsafe changes to legacy code (modifying globally-scoped variables, changing regex without understanding side effects, rewriting modules without preserving their interface contract).

These rules cover two audiences: teams writing new Perl (use modern features) and teams maintaining legacy Perl (make safe, incremental improvements without breaking production).

Rule 1: Modern Perl Defaults

The rule: 'Every new Perl file starts with: use v5.36; (or your minimum version). This enables strict, warnings, and signatures in one line. For pre-5.36: use strict; use warnings; at the top of every file — no exceptions. Never use bareword filehandles (open FH, "<", $file) — use lexical filehandles (open my $fh, "<", $file). Never use two-argument open — always three-argument.'

For modern features: 'Use subroutine signatures (sub greet($name) { }) instead of manual @_ unpacking. Use say instead of print with explicit newlines. Use // (defined-or) instead of || for default values. Use given/when or chained if/elsif for multi-branch logic. Use Path::Tiny for file operations instead of raw open/close/read.'

use strict and use warnings are the most impactful Perl rules. Without strict, typos in variable names create silent new variables. Without warnings, undefined values and other common bugs produce no output. One line (use v5.36;) prevents entire categories of Perl bugs.

  • use v5.36; (or use strict; use warnings; on older versions) — every file
  • Lexical filehandles: open my $fh — never bareword open FH
  • Three-argument open: open my $fh, '<', $file — never two-argument
  • Subroutine signatures over @_ unpacking — say over print with \n
  • // (defined-or) for defaults — Path::Tiny for file operations
⚠️ strict Is Non-Negotiable

Without use strict, a typo in a variable name silently creates a new variable. Without use warnings, undefined values produce no output. use v5.36; enables both — one line prevents entire categories of bugs.

Rule 2: Safe Legacy Code Maintenance

The rule: 'When modifying legacy code: never change more than one behavior per commit. Add tests before changing any existing code — verify the current behavior first, then modify. Never remove code you don't understand — comment it out with a dated note and revisit. Never change global variable names or subroutine signatures that other modules might depend on.'

For the strangler pattern: 'Wrap legacy code instead of rewriting it. Create new modules with clean interfaces that call the legacy code internally. New callers use the new interface. Over time, rewrite the internals while preserving the wrapper. This is safer than a big-bang rewrite — the legacy code continues to work while you replace it piece by piece.'

AI assistants want to rewrite entire modules to be 'cleaner.' In legacy codebases, a clean rewrite is the most dangerous change you can make — it breaks implicit dependencies, changes side effects that other code relies on, and removes behavior that looks like bugs but is actually relied upon by production systems.

💡 Don't Rewrite Legacy

AI wants to rewrite entire modules to be 'cleaner.' In legacy Perl, a clean rewrite is the most dangerous change — it breaks implicit dependencies and removes behavior that production relies on. Wrap and strangle instead.

Rule 3: OOP with Moo/Moose

The rule: 'For new OOP code, use Moo (lightweight) or Moose (full-featured) — never hand-rolled bless-based OOP. Moo provides: has for attributes, with for roles, extends for inheritance, and BUILD for construction. Use Type::Tiny for type constraints. Use namespace::autoclean to prevent namespace pollution.'

For attributes: 'Declare attributes with has: has name => (is => "ro", isa => Str, required => 1). Use "ro" (read-only) by default — "rw" only when mutation is genuinely needed. Use lazy + builder for expensive computed attributes. Use coerce for type transformation on input.'

AI assistants generate Perl OOP with manual bless, manual accessor methods, and manual type checking — patterns from the 1990s. Moo provides all of this in a clean, tested, and maintainable form. The generated code is shorter, safer, and familiar to every modern Perl developer.

Rule 4: Testing with Test2 or Test::More

The rule: 'Use Test2::V0 for new test files (modern, extensible). Use Test::More for projects already using it. Write test files in t/ directory: t/unit/module.t, t/integration/feature.t. Use done_testing() at the end of every test file. Use subtest for grouping related assertions. Mock external dependencies with Test2::Mock or Test::MockObject.'

For legacy code testing: 'Before modifying legacy code, write characterization tests — tests that capture the current behavior, even if you're not sure the behavior is correct. Run the characterization tests after your change. If they pass, you preserved existing behavior. If they fail, you changed something — determine if the change is intentional.'

For coverage: 'Use Devel::Cover for code coverage. Aim for high coverage on modules you're actively modifying — don't try to cover the entire legacy codebase at once. Use prove -l t/ to run tests with the lib/ directory in @INC.'

  • Test2::V0 for new tests — Test::More for existing test suites
  • t/ directory: t/unit/, t/integration/ — done_testing() in every file
  • Characterization tests before modifying legacy code — capture current behavior
  • subtest for grouping — Test2::Mock for mocking — Devel::Cover for coverage
  • prove -l t/ to run — --verbose for debugging — --jobs for parallel
ℹ️ Characterization Tests

Before modifying legacy code, write tests that capture current behavior — even if you're not sure it's correct. These tests tell you whether your change preserved existing behavior or broke something.

Rule 5: Dependencies and Project Structure

The rule: 'Use cpanfile for dependency declaration (like package.json). Use Carton for installing dependencies into a local directory (like node_modules). Pin versions in cpanfile: requires "Moo", "2.005005". Use Dist::Zilla or Module::Build for module packaging. Never install modules globally — always project-local with Carton.'

For project structure: 'lib/ for modules (lib/MyApp/User.pm), t/ for tests, bin/ for scripts, cpanfile for dependencies, and cpanfile.snapshot for locked versions. Use a standard directory layout that Perl tools expect — don't invent custom structures.'

For Docker: 'Use a multi-stage build: install dependencies in the builder, copy only the app and local/ (Carton's vendor dir) to the runtime image. Pin the Perl version in the FROM directive. Use carton exec to run the app with the local dependency tree.'

Complete Perl Rules Template

Consolidated rules for Perl projects — both new code and legacy maintenance.

  • use v5.36; (or use strict; use warnings;) — every file, no exceptions
  • Lexical filehandles, three-argument open, subroutine signatures
  • Legacy: one behavior change per commit, tests before changes, strangler pattern
  • Moo/Moose for OOP — never hand-rolled bless — Type::Tiny for constraints
  • Test2::V0 for tests — characterization tests before legacy modifications
  • cpanfile + Carton for dependencies — pin versions, project-local installs
  • Perl::Critic in CI — follow PBP (Perl Best Practices) or custom policy
  • perltidy for formatting — .perltidyrc committed to repo