diff --git a/README.md b/README.md
index b695f7a52e..c2924ba272 100644
--- a/README.md
+++ b/README.md
@@ -202,6 +202,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
| [template-no-action-on-submit-button](docs/rules/template-no-action-on-submit-button.md) | disallow action attribute on submit buttons | | | |
+| [template-no-args-paths](docs/rules/template-no-args-paths.md) | disallow args.foo paths in templates, use @foo instead | | 🔧 | |
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
| [template-no-at-ember-render-modifiers](docs/rules/template-no-at-ember-render-modifiers.md) | disallow usage of @ember/render-modifiers | | | |
diff --git a/docs/rules/template-no-args-paths.md b/docs/rules/template-no-args-paths.md
new file mode 100644
index 0000000000..c8753d8e4e
--- /dev/null
+++ b/docs/rules/template-no-args-paths.md
@@ -0,0 +1,80 @@
+# ember/template-no-args-paths
+
+🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
+
+
+
+Arguments that are passed to components are prefixed with the `@` symbol in Angle bracket syntax.
+Ember Octane leverages this in the component's templates by allowing users to directly refer to an argument using the same prefix:
+
+```gjs
+
+
+
+ {{#each @todos as |todo index|}}
+ -
+ {{yield (todo-item-component todo=todo) index}}
+
+ {{/each}}
+
+
+```
+
+We can immediately tell now by looking at this template that `@todos` is an argument that was passed to the component externally. This is in fact _always true_ - there is no way to modify the value referenced by `@todos` from the component class, it is the original, unmodified value.
+
+## Examples
+
+This rule **forbids** the following:
+
+```gjs
+
+ {{this.args.foo}}
+ {{args.foo}}
+
+```
+
+```gjs
+
+ {{my-helper this.args.foo}}
+ {{my-helper (hash value=this.args.foo)}}
+
+```
+
+```gjs
+
+
+
+
+```
+
+This rule **allows** the following:
+
+```gjs
+
+ {{my-helper this.args}}
+ {{my-helper (hash value=this.args)}}
+
+```
+
+```gjs
+
+ {{@foo}}
+
+
+
+```
+
+## Migration
+
+- find in templates `this.args.` replace to `@`
+
+## Related Rules
+
+- [no-curly-component-invocation](no-curly-component-invocation.md)
+
+## References
+
+- [RFC #276](https://github.com/emberjs/rfcs/blob/master/text/0276-named-args.md)
+- [Coming Soon in Ember Octane - Part 2: Named Argument Syntax](https://www.pzuraq.com/blog/coming-soon-in-ember-octane-part-2-angle-brackets-and-named-arguments/#namedargumentsyntax)
+- [Named arguments in Ember.js](https://www.balinterdi.com/blog/named-arguments-in-ember-js/)
+- [ember-named-arguments-polyfill](https://github.com/rwjblue/ember-named-arguments-polyfill)
diff --git a/lib/rules/template-no-args-paths.js b/lib/rules/template-no-args-paths.js
new file mode 100644
index 0000000000..e687bcb112
--- /dev/null
+++ b/lib/rules/template-no-args-paths.js
@@ -0,0 +1,97 @@
+/** @type {import('eslint').Rule.RuleModule} */
+module.exports = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'disallow args.foo paths in templates, use @foo instead',
+ category: 'Best Practices',
+ url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-args-paths.md',
+ templateMode: 'both',
+ },
+ fixable: 'code',
+ schema: [],
+ messages: {
+ argsPath:
+ 'Component templates should avoid "{{path}}" usage, try "@{{replacement}}" instead.',
+ },
+ originallyFrom: {
+ name: 'ember-template-lint',
+ rule: 'lib/rules/no-args-paths.js',
+ docs: 'docs/rule/no-args-paths.md',
+ tests: 'test/unit/rules/no-args-paths-test.js',
+ },
+ },
+ create(context) {
+ const localScopes = [];
+
+ function pushLocals(params) {
+ localScopes.push(new Set(params || []));
+ }
+
+ function popLocals() {
+ localScopes.pop();
+ }
+
+ function isLocal(name) {
+ for (const scope of localScopes) {
+ if (scope.has(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ return {
+ GlimmerBlockStatement(node) {
+ if (node.program && node.program.blockParams) {
+ pushLocals(node.program.blockParams);
+ }
+ },
+ 'GlimmerBlockStatement:exit'(node) {
+ if (node.program && node.program.blockParams) {
+ popLocals();
+ }
+ },
+
+ GlimmerElementNode(node) {
+ if (node.blockParams && node.blockParams.length > 0) {
+ pushLocals(node.blockParams);
+ }
+ },
+ 'GlimmerElementNode:exit'(node) {
+ if (node.blockParams && node.blockParams.length > 0) {
+ popLocals();
+ }
+ },
+
+ GlimmerPathExpression(node) {
+ const path = node.original;
+
+ // @args.foo is a valid named argument — skip paths starting with @
+ if (node.head?.type === 'AtHead') {
+ return;
+ }
+
+ if (!path?.startsWith('args.') && !path?.startsWith('this.args.')) {
+ return;
+ }
+
+ // Skip when 'args' is a block param in the current scope
+ if (isLocal('args')) {
+ return;
+ }
+
+ const replacement = path.replace(/^(this\.)?args\./, '');
+
+ context.report({
+ node,
+ messageId: 'argsPath',
+ data: { path, replacement },
+ fix(fixer) {
+ return fixer.replaceText(node, `@${replacement}`);
+ },
+ });
+ },
+ };
+ },
+};
diff --git a/tests/lib/rules/template-no-args-paths.js b/tests/lib/rules/template-no-args-paths.js
new file mode 100644
index 0000000000..649468c2f9
--- /dev/null
+++ b/tests/lib/rules/template-no-args-paths.js
@@ -0,0 +1,133 @@
+const rule = require('../../../lib/rules/template-no-args-paths');
+const RuleTester = require('eslint').RuleTester;
+
+const ruleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser'),
+ parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
+});
+ruleTester.run('template-no-args-paths', rule, {
+ valid: [
+ '{{@foo}}',
+ // @args.foo is a valid named argument, not a path violation
+ '{{@args.foo}}',
+ '',
+ '{{foo (name this.args)}}',
+ '{{foo name=this.args}}',
+ '{{foo name=(extract this.args)}}',
+ '',
+ '',
+ '',
+ '',
+ // args as a block param is not flagged
+ '{{#each items as |args|}}{{args.name}}{{/each}}',
+ ],
+ invalid: [
+ {
+ code: '{{hello (format value=args.foo)}}',
+ output: '{{hello (format value=@foo)}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello value=args.foo}}',
+ output: '{{hello value=@foo}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello (format args.foo.bar)}}',
+ output: '{{hello (format @foo.bar)}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '
',
+ output: '
',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello args.foo.bar}}',
+ output: '{{hello @foo.bar}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{args.foo.bar}}',
+ output: '{{@foo.bar}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{args.foo}}',
+ output: '{{@foo}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{this.args.foo}}',
+ output: '{{@foo}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ ],
+});
+
+const hbsRuleTester = new RuleTester({
+ parser: require.resolve('ember-eslint-parser/hbs'),
+ parserOptions: {
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+});
+
+hbsRuleTester.run('template-no-args-paths', rule, {
+ valid: [
+ // @args.foo is a valid named argument
+ '{{@args.foo}}',
+ '
',
+ '{{foo (name this.args)}}',
+ '{{foo name=this.args}}',
+ '{{foo name=(extract this.args)}}',
+ '',
+ '',
+ '',
+ '',
+ // args as a block param is not flagged
+ '{{#each items as |args|}}{{args.name}}{{/each}}',
+ ],
+ invalid: [
+ {
+ code: '{{hello (format value=args.foo)}}',
+ output: '{{hello (format value=@foo)}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello value=args.foo}}',
+ output: '{{hello value=@foo}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello (format args.foo.bar)}}',
+ output: '{{hello (format @foo.bar)}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '
',
+ output: '
',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{hello args.foo.bar}}',
+ output: '{{hello @foo.bar}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{args.foo.bar}}',
+ output: '{{@foo.bar}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{args.foo}}',
+ output: '{{@foo}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ {
+ code: '{{this.args.foo}}',
+ output: '{{@foo}}',
+ errors: [{ messageId: 'argsPath' }],
+ },
+ ],
+});