diff --git a/src/main/java/org/eolang/lints/LtMystery.java b/src/main/java/org/eolang/lints/LtMystery.java
new file mode 100644
index 000000000..9c6aeac2e
--- /dev/null
+++ b/src/main/java/org/eolang/lints/LtMystery.java
@@ -0,0 +1,163 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
+ * SPDX-License-Identifier: MIT
+ */
+package org.eolang.lints;
+
+import com.github.lombrozo.xnav.Xnav;
+import com.jcabi.xml.XML;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Lint to catch mystery objects.
+ *
+ *
A mystery object is a free application of an object that is neither
+ * declared anywhere in the program nor is one of the {@code org.eolang}
+ * prime objects (like {@code number} or {@code bytes}). It almost always
+ * means a typo in the name of an object or a missing {@code +alias} meta.
+ * Such a call is compiled to {@code Φ.NAME} reference, which this lint
+ * inspects.
+ *
+ * @since 0.0.52
+ */
+final class LtMystery implements Lint {
+
+ /**
+ * Reserved names.
+ * The key is object name, the value is the path to EO file.
+ */
+ private final Map reserved;
+
+ /**
+ * Ctor.
+ */
+ LtMystery() {
+ this(new ReservedNames());
+ }
+
+ /**
+ * Ctor.
+ * @param names Reserved names
+ */
+ LtMystery(final Map names) {
+ this.reserved = names;
+ }
+
+ @Override
+ public String name() {
+ return "mystery-object";
+ }
+
+ @Override
+ public Collection defects(final XML xmir) throws IOException {
+ final Collection defects;
+ if (this.reserved.isEmpty()) {
+ defects = new ArrayList<>(0);
+ } else {
+ defects = LtMystery.find(xmir, this.reserved);
+ }
+ return defects;
+ }
+
+ @Override
+ public String motive() throws IOException {
+ return new MotiveFrom("names", this.name()).asString();
+ }
+
+ @Override
+ public Fix fix() {
+ return new FxEmpty();
+ }
+
+ /**
+ * Detect mystery objects in the program.
+ * @param xmir XMIR document
+ * @param reserved Reserved org.eolang names
+ * @return Detected defects
+ */
+ private static Collection find(final XML xmir,
+ final Map reserved) {
+ final Xnav xnav = new Xnav(xmir.inner());
+ final Set declared = xnav.path("//o[@name]").map(
+ elem -> elem.attribute("name").text().get()
+ ).collect(Collectors.toSet());
+ return xnav.path("//o[@base]").filter(
+ LtMystery::simpleBase
+ ).filter(
+ elem -> LtMystery.unknown(elem, declared, reserved)
+ ).map(
+ LtMystery::defect
+ ).collect(Collectors.toList());
+ }
+
+ /**
+ * Is the {@code @base} a bare single-object reference?
+ * @param elem Object element
+ * @return True if it's a simple object reference
+ */
+ private static boolean simpleBase(final Xnav elem) {
+ final Optional base = elem.attribute("base").text();
+ return base.isPresent()
+ && LtMystery.simple(base.get());
+ }
+
+ /**
+ * Is the referenced object unknown?
+ * The name is the {@code @base} value without the leading two characters,
+ * which are the {@code Φ.} prefix.
+ * @param elem Object element
+ * @param declared Objects declared in the program
+ * @param reserved Reserved org.eolang names
+ * @return True if the object is neither declared nor reserved
+ */
+ private static boolean unknown(final Xnav elem,
+ final Set declared, final Map reserved) {
+ final String name = LtMystery.objectName(elem);
+ return !declared.contains(name)
+ && !reserved.containsKey(name);
+ }
+
+ /**
+ * Object name from the {@code @base} attribute.
+ * The name is the value without the leading two characters,
+ * which are the {@code Φ.} prefix.
+ * @param elem Object element
+ * @return Object name
+ */
+ private static String objectName(final Xnav elem) {
+ return elem.attribute("base").text().get().substring(2);
+ }
+
+ /**
+ * Defect for an unknown object.
+ * @param elem Object element
+ * @return Defect
+ */
+ private static Defect defect(final Xnav elem) {
+ return new Defect.Default(
+ "mystery-object",
+ Severity.ERROR,
+ new LineOf(elem).value(),
+ String.format(
+ "Object \"%s\" is not defined in the program and is not part of org.eolang",
+ LtMystery.objectName(elem)
+ )
+ );
+ }
+
+ /**
+ * Check if the base is a reference to a single object, like
+ * {@code Φ.bar}, and not to a path like {@code Φ.org.eolang.io.stdout}.
+ * @param base Base attribute value
+ * @return True if it's a simple object reference
+ */
+ private static boolean simple(final String base) {
+ return base.matches("^Φ\\.[a-z][a-z0-9_-]*$");
+ }
+}
diff --git a/src/main/java/org/eolang/lints/MonoLints.java b/src/main/java/org/eolang/lints/MonoLints.java
index 1ce9c4bb9..246a149db 100644
--- a/src/main/java/org/eolang/lints/MonoLints.java
+++ b/src/main/java/org/eolang/lints/MonoLints.java
@@ -27,7 +27,8 @@ final class MonoLints extends IterableEnvelope {
List.of(
new LtAsciiOnly(),
new LtReservedName(),
- new LtSyntaxVersion()
+ new LtSyntaxVersion(),
+ new LtMystery()
)
)
);
diff --git a/src/main/resources/org/eolang/motives/names/mystery-object.md b/src/main/resources/org/eolang/motives/names/mystery-object.md
new file mode 100644
index 000000000..d6677ee5c
--- /dev/null
+++ b/src/main/resources/org/eolang/motives/names/mystery-object.md
@@ -0,0 +1,20 @@
+# Mystery object
+
+Objects, applied in the code, must be defined somewhere in the program,
+be the objects of `org.eolang.*` or be imported via `+alias` meta. Free
+usage of an unknown object is a mystery object—it is almost always a
+typo in the name, since this object is not declared anywhere.
+
+Incorrect:
+
+```eo
+# Foo.
+[] > foo
+ bar 42 > x
+```
+
+Here, `bar` is not defined in the program, not an object from `org.eolang.*`
+and not imported via `+alias`. It will be compiled to `Φ.bar` reference,
+which can't be validated by the parser. This is what we call a mystery
+object, and it should be fixed by adding the definition of `bar`, importing
+it, or using a proper object name.
diff --git a/src/test/java/org/eolang/lints/LtMysteryTest.java b/src/test/java/org/eolang/lints/LtMysteryTest.java
new file mode 100644
index 000000000..850b9481c
--- /dev/null
+++ b/src/test/java/org/eolang/lints/LtMysteryTest.java
@@ -0,0 +1,121 @@
+/*
+ * SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
+ * SPDX-License-Identifier: MIT
+ */
+package org.eolang.lints;
+
+import fixtures.EoProgram;
+import java.io.IOException;
+import java.util.Map;
+import org.cactoos.io.InputOf;
+import org.cactoos.list.ListOf;
+import org.cactoos.map.MapEntry;
+import org.cactoos.map.MapOf;
+import org.hamcrest.MatcherAssert;
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Tests for {@link LtMystery}.
+ * @since 0.0.52
+ */
+final class LtMysteryTest {
+
+ @Test
+ void catchesMysteryObject() throws IOException {
+ MatcherAssert.assertThat(
+ "It is expected to catch a mystery object here",
+ new LtMystery(this.canonical()).defects(
+ new EoProgram("org/eolang/lints/mystery-object.eo").parse()
+ ),
+ Matchers.hasSize(1)
+ );
+ }
+
+ @Test
+ void allowsDeclaredObject() throws IOException {
+ MatcherAssert.assertThat(
+ "Defects are not empty, but the object is declared in the program",
+ new LtMystery(this.canonical()).defects(
+ new EoProgram("org/eolang/lints/declared-object.eo").parse()
+ ),
+ Matchers.emptyIterable()
+ );
+ }
+
+ @Test
+ void allowsAliasedObject() throws IOException {
+ MatcherAssert.assertThat(
+ "Defects are not empty, but the object is imported via +alias",
+ new LtMystery(this.canonical()).defects(
+ new EoProgram("org/eolang/lints/aliased-object.eo").parse()
+ ),
+ Matchers.emptyIterable()
+ );
+ }
+
+ @Test
+ void allowsCanonicalObject() throws IOException {
+ MatcherAssert.assertThat(
+ "Defects are not empty, but the object is part of org.eolang",
+ new LtMystery(this.canonical()).defects(
+ new EoProgram("org/eolang/lints/canonical-object.eo").parse()
+ ),
+ Matchers.emptyIterable()
+ );
+ }
+
+ @Test
+ void reportsCorrectMessageForMysteryObject() throws IOException {
+ MatcherAssert.assertThat(
+ "The message should mention the mystery object",
+ new ListOf<>(
+ new LtMystery(this.canonical()).defects(
+ new EoProgram("org/eolang/lints/mystery-object.eo").parse()
+ )
+ ).get(0).text(),
+ Matchers.equalTo(
+ "Object \"bar\" is not defined in the program and is not part of org.eolang"
+ )
+ );
+ }
+
+ @Test
+ void allowsAllObjectsDeclaredInline() throws IOException {
+ final String src = String.format(
+ "[] > foo%n [a] > bar%n bar > x"
+ );
+ MatcherAssert.assertThat(
+ "Objects should not be reported, since all of them are declared",
+ new LtMystery(this.canonical()).defects(
+ new EoProgram(src, new InputOf(src)).parse()
+ ),
+ Matchers.emptyIterable()
+ );
+ }
+
+ @Tag("reserved")
+ @Test
+ void scansMysteryFromHome() throws Exception {
+ MatcherAssert.assertThat(
+ "It is expected to catch a mystery object using reserved names from home",
+ new LtMystery().defects(
+ new EoProgram("org/eolang/lints/mystery-bipki.eo").parse()
+ ),
+ Matchers.hasSize(1)
+ );
+ }
+
+ /**
+ * Dummy reserved names for the tests.
+ * @return Reserved names map
+ */
+ private Map canonical() {
+ return new MapOf<>(
+ new MapEntry<>("number", "number.eo"),
+ new MapEntry<>("bytes", "bytes.eo"),
+ new MapEntry<>("string", "string.eo")
+ );
+ }
+}
diff --git a/src/test/resources/org/eolang/lints/aliased-object.eo b/src/test/resources/org/eolang/lints/aliased-object.eo
new file mode 100644
index 000000000..bc7dc8c08
--- /dev/null
+++ b/src/test/resources/org/eolang/lints/aliased-object.eo
@@ -0,0 +1,7 @@
++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
++spdx SPDX-License-Identifier: MIT
++alias org.eolang.io.stdout
+
+# Foo.
+[] > foo
+ stdout "Hello" > x
diff --git a/src/test/resources/org/eolang/lints/canonical-object.eo b/src/test/resources/org/eolang/lints/canonical-object.eo
new file mode 100644
index 000000000..6c0ae5309
--- /dev/null
+++ b/src/test/resources/org/eolang/lints/canonical-object.eo
@@ -0,0 +1,6 @@
++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
++spdx SPDX-License-Identifier: MIT
+
+# Foo.
+[] > foo
+ 42 > x
diff --git a/src/test/resources/org/eolang/lints/declared-object.eo b/src/test/resources/org/eolang/lints/declared-object.eo
new file mode 100644
index 000000000..520cdcb12
--- /dev/null
+++ b/src/test/resources/org/eolang/lints/declared-object.eo
@@ -0,0 +1,7 @@
++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
++spdx SPDX-License-Identifier: MIT
+
+# Foo.
+[] > foo
+ [t] > bar
+ bar 42 > x
diff --git a/src/test/resources/org/eolang/lints/mystery-bipki.eo b/src/test/resources/org/eolang/lints/mystery-bipki.eo
new file mode 100644
index 000000000..1e7f67a9b
--- /dev/null
+++ b/src/test/resources/org/eolang/lints/mystery-bipki.eo
@@ -0,0 +1,6 @@
++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
++spdx SPDX-License-Identifier: MIT
+
+# Foo.
+[] > foo
+ bipki 42 > x
\ No newline at end of file
diff --git a/src/test/resources/org/eolang/lints/mystery-object.eo b/src/test/resources/org/eolang/lints/mystery-object.eo
new file mode 100644
index 000000000..c00d8c7ae
--- /dev/null
+++ b/src/test/resources/org/eolang/lints/mystery-object.eo
@@ -0,0 +1,6 @@
++spdx SPDX-FileCopyrightText: Copyright (c) 2016-2026 Objectionary.com
++spdx SPDX-License-Identifier: MIT
+
+# Foo.
+[] > foo
+ bar 42 > x