In a bank, the first question about an AI coding assistant is rarely 'what can this tool do?' It is 'what can we make sure it never does?' That framing shaped everything when I configured my team's assistant at BMO, GitHub Copilot Business. Guardrails in a banking environment are not about limiting the tool. They are about making the tool trustworthy enough to actually use on a regulated codebase.
What follows is the approach: the layers I rely on, the categories of concern worth covering, and what a configuration like this looks like in practice. The files in this post are illustrative examples I wrote for it, not the bank's configuration, which stays internal. What transfers to your own codebase is the structure and the reasoning.
What Guardrails Actually Mean in a Regulated Environment
In a consumer tech startup, AI guardrails might mean 'do not let Copilot suggest insecure random number generation.' In a bank, the scope is considerably wider. A Canadian bank operates under OSFI guidelines, FINTRAC requirements, PCI DSS for payment card data, and data classification policies that determine what code can do with what categories of information.
The threat model for AI-assisted coding in banking has three layers. First, data exfiltration risk: code that inadvertently sends customer data to external services. Second, security anti-patterns: suggestions that introduce known vulnerabilities, such as hardcoded credentials, insufficient input validation, or insecure direct object references. Third, compliance violations: code that bypasses audit logging, ignores consent flags, or processes data in ways that violate data residency requirements.
None of these are hypothetical. Without instructions, an AI assistant will readily suggest localStorage for storing a user session token (a security anti-pattern in a banking context), a direct HTTP call to a third-party API without proxy routing (a data governance issue), or a console.log statement that prints a whole customer object (a PII logging violation). Code review catches these. Guardrails are about catching them before they reach a human reviewer.
Repository Custom Instructions in Copilot
Copilot reads repository-wide custom instructions from .github/copilot-instructions.md, a plain Markdown file of natural-language instructions committed with the code. Think of it as a system prompt that travels with the repository and is added to every chat request. GitHub documents it for Copilot Chat, code review and the cloud agent, and the VS Code documentation says custom instructions are not taken into account for inline suggestions as you type, which is one reason the lint layer below matters. This is still where the most impactful guardrails live.
A file like this works best split across six concerns: project context, security rules, data handling, Angular-specific patterns, code quality standards, and things the AI must never do. The 'never do' section is the most important. It is explicit and imperative. The example below follows that structure. Everything in it, including every service name, is invented for this post, so treat it as a shape to fill in rather than a rule set to copy.
<!-- .github/copilot-instructions.md -->
<!-- ILLUSTRATIVE. Written for this blog post, invented end to end. -->
<!-- Every service name below is made up. This is not BMO's file, and the -->
<!-- rules are the kind a bank would write, not the ones any bank did write. -->
# Project context
This is an enterprise Angular application for a regulated financial institution.
- Angular version: <your exact major version>
- State management: NgRx with strict action hygiene.
- Styling: SCSS with design tokens via CSS custom properties.
- Testing: Jest for unit tests.
- All API calls go through ApiClientService, never directly via HttpClient.
# Security rules (mandatory)
- Never suggest storing sensitive data (tokens, credentials, PII, account numbers) in localStorage or sessionStorage.
- Never generate code that logs objects containing user data, account information, or transaction details.
- Never suggest direct third-party API calls. All external communication goes through ApiClientService.
- Never generate hardcoded credentials, API keys, environment-specific URLs, or secrets of any kind.
- Never suggest eval(), Function(), or other dynamic code execution.
- Validate and sanitize input on the client, and never assume the client check replaces the server one.
- Tokens live in httpOnly cookies managed by AuthService. Do not suggest alternatives.
# Data handling
- Display PII fields (SIN, date of birth, account numbers, name plus address combinations) through PiiMaskingService.
- Audit-sensitive operations call AuditService.record() before the operation runs.
- Check consent through ConsentService before processing marketing-related data.
- Never suggest patterns that send customer data outside approved service boundaries.
# Angular patterns
- Match the component style already used in the surrounding code. Do not mix styles in one feature.
- OnPush change detection is the default. Justify any use of the Default strategy.
- Every subscription needs a teardown. Prefer AsyncPipe, or the teardown pattern the codebase already uses.
- Route errors through the global ErrorHandler, not ad-hoc try/catch with console.error.
- Feature flags go through FeatureFlagService.isEnabled(). Never hardcode feature conditionals.
- Avoid any. If you cannot type something precisely, use unknown and narrow it.
# Code quality
- Public methods have JSDoc comments.
- Flag functions longer than 30 lines for decomposition.
- Magic numbers and strings become named constants.
- New services come with a *.spec.ts that meets the coverage threshold enforced in CI.
# Never do
- Do not suggest code that bypasses the established service layer.
- Do not generate commented-out code blocks.
- Do not add TODO comments without a ticket reference.
- Do not propose architectural changes without flagging them as architectural suggestions that need review.Path-Specific Instructions and Content Exclusion
Rules that only matter for part of the codebase go in path-specific files under .github/instructions/, named NAME.instructions.md, with an applyTo glob in the frontmatter. When a file matches, Copilot combines those instructions with the repository-wide ones. Test conventions are a good candidate: they matter in every spec file and nowhere else. Here is an illustrative .github/instructions/testing.instructions.md:
---
applyTo: "**/*.spec.ts"
---
<!-- ILLUSTRATIVE. Written for this blog post, not a real bank configuration. -->
- Unit tests use Jest. Follow the describe/it structure of the neighboring spec files.
- Cover the happy path, boundary values, null and undefined inputs, and the error branch.
- Never put realistic customer data in fixtures. Use values that are obviously fake.
- Mock HTTP at the service boundary. A unit test never calls a real endpoint.
- Every assertion checks behavior. A test that only proves the component was created does not count as coverage.Instructions shape what Copilot writes. Content exclusion controls what it can see. On the Business and Enterprise plans, repository administrators and organization owners can exclude files and directories so Copilot does not use them as context. Exclusions are entered in the GitHub UI (repository Settings, then Copilot, then Content exclusion, or the organization equivalent), not in a committed file. That makes them the right tool for the most sensitive implementation files. An illustrative repository-level list:
# Illustrative repository-level content exclusion, not a real configuration.
# Entered in the GitHub UI under Settings > Copilot > Content exclusion.
# Mirrored here for documentation only. Copilot does not read this file.
- "/src/environments/**" # environment configuration
- "**/secrets/**" # secret management utilities
- "*.pem"
- "*.key"
- "*.env*"The Layers That Actually Enforce: ESLint and the CI Gate
Guardrails cannot rely on the AI respecting instructions, because instructions are a request and a model is free to ignore a request. The lint layer catches the anti-patterns at the code level regardless of who or what typed them. One detail that matters more than it looks: write the rule against the AST, not against a string. A naive version that only inspects a bare identifier misses window.localStorage and computed access like window['localStorage'], which is exactly the form a model reaches for when it has been told not to use the plain one.
// eslint-rules/no-web-storage.js (illustrative example)
// Disallow localStorage/sessionStorage. Session tokens live in httpOnly
// cookies managed by AuthService, which is what the instructions file says too.
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow direct localStorage/sessionStorage access',
category: 'Security'
},
messages: {
noWebStorage:
'Do not use {{ storage }} directly. Tokens live in httpOnly cookies managed by AuthService. See: <link to your storage guidelines>'
}
},
create(context) {
const STORAGES = ['localStorage', 'sessionStorage'];
// Matches localStorage.x, window.localStorage.x and window['localStorage'].x
const storageName = (object) => {
if (object.type === 'Identifier' && STORAGES.includes(object.name)) {
return object.name;
}
if (object.type === 'MemberExpression') {
const key = object.computed
? (object.property.type === 'Literal' ? object.property.value : null)
: object.property.name;
return STORAGES.includes(key) ? key : null;
}
return null;
};
return {
MemberExpression(node) {
const storage = storageName(node.object);
if (!storage) return;
context.report({ node, messageId: 'noWebStorage', data: { storage } });
}
};
}
};ESLint is only half of the enforcement, and the smaller half. The other half is the CI gate, and that is the layer that decides what merges. Lint and the Jest coverage threshold run on every pull request, and SonarQube quality gates run alongside them. A failing gate blocks the merge, and it does not care whether a person or a model wrote the line. That is the whole distinction: the instructions file is guidance, and guidance can be ignored, while the pipeline is not negotiable. If your team only has the appetite to build one layer this quarter, build that one. Instructions without a gate behind them are a style guide.
What Rules Like These Prevent
The categories are predictable. In front-end code written with AI help, the same four come up again and again: direct localStorage usage for auth state, console.log calls that serialize whole response payloads, direct HttpClient usage that bypasses the service layer, and annotations typed as any, which weaken type safety.
These are not hypothetical security bugs. They are the patterns AI tools default to, because generic Angular tutorials are full of them. Guardrails do not make the tools less useful. They make the output trustworthy enough to include in code review rather than treating every AI suggestion as suspect by default.
The Limits of Guardrails
I want to be clear about what guardrails cannot do. They cannot prevent architectural mistakes. They cannot catch business logic errors in complex domain rules. They cannot verify that an AI-generated algorithm produces correct outputs for edge cases. They cannot replace code review. What they do is enforce a floor: a set of non-negotiable patterns that should never appear regardless of the task.
- Guardrails are a floor, not a ceiling. They prevent the worst outcomes, not all the suboptimal ones
- Custom instructions are suggestions to the AI, not hard constraints, and they do not reach inline suggestions at all. The pipeline, lint plus tests plus quality gates, is the actual enforcement
- Guardrails need updating as the codebase evolves. Outdated rules create confusion and then get ignored
- Engineers need to know why each rule exists. Rules without a rationale get worked around
- Content exclusion is the most reliable control over what Copilot can see, which is a different question from what ships. Excluded content is not used as context. GitHub documents that Agent mode in Copilot Chat does not support content exclusion, so check which modes your team actually uses
The goal is not to cage the AI. The goal is to make the AI's suggestions trustworthy enough that developers can focus their review energy on logic and business correctness, not on catching security anti-patterns that linting should catch automatically.
If you are setting up AI tools in a regulated environment, start with the 'never do' list. What are the three categories of code that, if generated and merged without review, would cause an incident? Write those rules first, enforce them with linting, and build the positive guidance from there. Setting this up is not a quarter-long project, and it is never finished either. I kept refining it as the codebase and the tools changed. It is scoped to one team, not an organization-wide template. That is enough to change how a team of five developers works, and it is a better starting point than trying to standardize the whole organization before anyone has used the rules in anger.
References
- GitHub Copilot content exclusion. https://docs.github.com/en/copilot/how-tos/configure-content-exclusion/exclude-content-from-copilot
- GitHub Copilot custom instructions. https://docs.github.com/en/copilot/customizing-copilot/adding-repository-custom-instructions-for-github-copilot
- OSFI Technology and Cyber Risk. https://www.osfi-bsif.gc.ca/en/guidance/guidance-library/technology-cyber-risk-management
- VS Code custom instructions for Copilot. https://code.visualstudio.com/docs/agent-customization/custom-instructions