diff --git a/README.md b/README.md
index c2924ba272..fe57ea27d1 100644
--- a/README.md
+++ b/README.md
@@ -215,6 +215,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
| [template-no-dynamic-subexpression-invocations](docs/rules/template-no-dynamic-subexpression-invocations.md) | disallow dynamic subexpression invocations | | | |
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
+| [template-no-forbidden-elements](docs/rules/template-no-forbidden-elements.md) | disallow specific HTML elements | | | |
| [template-no-html-comments](docs/rules/template-no-html-comments.md) | disallow HTML comments in templates | | 🔧 | |
| [template-no-implicit-this](docs/rules/template-no-implicit-this.md) | require explicit `this` in property access | | | |
| [template-no-index-component-invocation](docs/rules/template-no-index-component-invocation.md) | disallow index component invocations | | | |
diff --git a/docs/rules/template-no-forbidden-elements.md b/docs/rules/template-no-forbidden-elements.md
new file mode 100644
index 0000000000..5fd00a24e8
--- /dev/null
+++ b/docs/rules/template-no-forbidden-elements.md
@@ -0,0 +1,57 @@
+# ember/template-no-forbidden-elements
+
+
+
+This rule disallows the use of forbidden elements in template files.
+
+The rule is configurable so teams can add their own disallowed elements.
+The default list of forbidden elements are `meta`, `style`, `html`, and `script`.
+
+## Examples
+
+This rule **forbids** the following:
+
+```gjs
+
+```
+
+```gjs
+
+```
+
+```gjs
+
+```
+
+```gjs
+
+```
+
+This rule **allows** the following:
+
+```gjs
+
+```
+
+```gjs
+
+```
+
+```gjs
+
+
+
+
+
+```
+
+Note: `` inside `
` is allowed as an exception.
+
+## Configuration
+
+- `boolean` — `true` to enable with defaults / `false` to disable
+- `string[]` — an array of element names to forbid (default: `['meta', 'style', 'html', 'script']`)
+
+## References
+
+- [Ember guides/template restrictions](https://guides.emberjs.com/release/components/#toc_restrictions)
diff --git a/lib/rules/template-no-forbidden-elements.js b/lib/rules/template-no-forbidden-elements.js
new file mode 100644
index 0000000000..c9780a93b1
--- /dev/null
+++ b/lib/rules/template-no-forbidden-elements.js
@@ -0,0 +1,77 @@
+const DEFAULT_FORBIDDEN = ['meta', 'style', 'html', 'script'];
+
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'disallow specific HTML elements',
+ category: 'Best Practices',
+ recommendedGjs: false,
+ recommendedGts: false,
+ url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-forbidden-elements.md',
+ templateMode: 'both',
+ },
+ schema: [
+ {
+ oneOf: [
+ { type: 'array', items: { type: 'string' } },
+ { type: 'boolean' },
+ {
+ type: 'object',
+ properties: {
+ forbidden: { type: 'array', items: { type: 'string' } },
+ },
+ additionalProperties: false,
+ },
+ ],
+ },
+ ],
+ messages: { forbidden: 'Use of forbidden element <{{element}}>' },
+ originallyFrom: {
+ name: 'ember-template-lint',
+ rule: 'lib/rules/no-forbidden-elements.js',
+ docs: 'docs/rule/no-forbidden-elements.md',
+ tests: 'test/unit/rules/no-forbidden-elements-test.js',
+ },
+ },
+ create(context) {
+ const rawConfig = context.options[0];
+ let forbiddenList;
+
+ if (rawConfig === true || rawConfig === undefined) {
+ forbiddenList = DEFAULT_FORBIDDEN;
+ } else if (Array.isArray(rawConfig)) {
+ forbiddenList = rawConfig;
+ } else if (rawConfig && typeof rawConfig === 'object') {
+ forbiddenList = rawConfig.forbidden ?? DEFAULT_FORBIDDEN;
+ } else {
+ forbiddenList = [];
+ }
+
+ const forbidden = new Set(forbiddenList);
+
+ // Track element stack for in exception
+ const elementStack = [];
+
+ return {
+ GlimmerElementNode(node) {
+ elementStack.push(node.tag);
+
+ if (!forbidden.has(node.tag)) {
+ return;
+ }
+
+ // Exception: inside is allowed
+ if (node.tag === 'meta' && elementStack.includes('head')) {
+ return;
+ }
+
+ context.report({ node, messageId: 'forbidden', data: { element: node.tag } });
+ },
+ 'GlimmerElementNode:exit'() {
+ elementStack.pop();
+ },
+ };
+ },
+};
diff --git a/tests/lib/rules/template-no-forbidden-elements.js b/tests/lib/rules/template-no-forbidden-elements.js
new file mode 100644
index 0000000000..2361ef3ffe
--- /dev/null
+++ b/tests/lib/rules/template-no-forbidden-elements.js
@@ -0,0 +1,129 @@
+const rule = require('../../../lib/rules/template-no-forbidden-elements');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+ruleTester.run('template-no-forbidden-elements', rule, {
+ valid: [
+ { code: '', options: [['script']] },
+ // Object config form
+ { code: '', options: [{ forbidden: ['script'] }] },
+ { code: '', options: [{ forbidden: ['html'] }] },
+ '',
+ '',
+ '',
+ '',
+ ],
+ invalid: [
+ {
+ code: '',
+ output: null,
+ options: [{ forbidden: ['script'] }],
+ errors: [{ messageId: 'forbidden' }],
+ },
+ {
+ code: '',
+ output: null,
+ options: [['script']],
+ errors: [{ messageId: 'forbidden' }],
+ },
+
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'forbidden' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'forbidden' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'forbidden' }],
+ },
+ {
+ code: '',
+ output: null,
+ errors: [{ messageId: 'forbidden' }],
+ },
+ {
+ code: '',
+ output: null,
+ options: [['Foo']],
+ errors: [{ messageId: 'forbidden' }],
+ },
+ ],
+});
+
+const hbsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser/hbs'),
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+});
+
+hbsRuleTester.run('template-no-forbidden-elements', rule, {
+ valid: [
+ '',
+ '',
+ '',
+ '',
+ '',
+ // Custom forbidden list (script not included).
+ {
+ code: '',
+ options: [['html', 'meta', 'style']],
+ },
+ // Object config form.
+ {
+ code: '',
+ options: [{ forbidden: ['html', 'meta', 'style'] }],
+ },
+ ],
+ invalid: [
+ // Default config.
+ {
+ code: '',
+ output: null,
+ errors: [{ message: 'Use of forbidden element