diff --git a/src/godotiq/cli.py b/src/godotiq/cli.py
index a6ce657..049e3be 100644
--- a/src/godotiq/cli.py
+++ b/src/godotiq/cli.py
@@ -49,11 +49,13 @@ def _serve() -> None:
     main()
 
 
+_ADDON_EXTENSIONS = {".gd", ".cfg"}
+
+
 def install_addon(target_dir: str, dry_run: bool = False) -> None:
-    """Copy addon files and GODOTIQ_RULES.md to a Godot project.
+    """Copy addon files and GODOTIQ_RULES.md to a Godot project."""
+    import shutil
 
-    Full implementation in section-04.
-    """
     target = Path(target_dir).resolve()
     if not target.is_dir():
         print(f"Error: directory does not exist: {target}", file=sys.stderr)
@@ -64,5 +66,52 @@ def install_addon(target_dir: str, dry_run: bool = False) -> None:
             file=sys.stderr,
         )
         sys.exit(1)
-    print("install-addon: not yet implemented (section-04)", file=sys.stderr)
-    sys.exit(1)
+
+    # Locate source files
+    pkg_dir = Path(__file__).resolve().parent
+    addon_src = pkg_dir / "addon"
+    rules_src = pkg_dir / "prompts" / "godot_development.md"
+
+    # In editable installs, force-include files aren't in addon/.
+    # Fall back to the repo's godot-addon/ directory.
+    if not any(f.suffix in _ADDON_EXTENSIONS for f in addon_src.iterdir() if f.is_file()):
+        repo_addon = pkg_dir.parent.parent / "godot-addon" / "addons" / "godotiq"
+        if repo_addon.is_dir():
+            addon_src = repo_addon
+
+    # Build copy manifest: (source, destination, relative_display_path)
+    manifest: list[tuple[Path, Path, str]] = []
+
+    for f in sorted(addon_src.iterdir()):
+        if f.suffix in _ADDON_EXTENSIONS:
+            dst = target / "addons" / "godotiq" / f.name
+            manifest.append((f, dst, f"addons/godotiq/{f.name}"))
+
+    if rules_src.exists():
+        manifest.append((rules_src, target / "GODOTIQ_RULES.md", "GODOTIQ_RULES.md"))
+
+    # Dry-run mode
+    if dry_run:
+        print(f"[dry-run] Would install GodotIQ addon to {target}")
+        for _, _, rel in manifest:
+            print(f"  would copy: {rel}")
+        return
+
+    # Create destination directory
+    (target / "addons" / "godotiq").mkdir(parents=True, exist_ok=True)
+
+    # Copy files and report
+    print(f"Installing GodotIQ addon to {target} ...")
+    added = 0
+    updated = 0
+    for src, dst, rel in manifest:
+        existed = dst.exists()
+        shutil.copy2(src, dst)
+        status = "updated" if existed else "added"
+        if existed:
+            updated += 1
+        else:
+            added += 1
+        print(f"  {status:>9s}: {rel}")
+
+    print(f"\nDone. {added} files added, {updated} files updated.")
diff --git a/tests/test_cli.py b/tests/test_cli.py
index f2211af..c257417 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -2,9 +2,12 @@
 
 import subprocess
 import sys
+from pathlib import Path
 
 import pytest
 
+from godotiq.cli import install_addon
+
 
 class TestCliParsing:
     """Test argparse routing in cli.py."""
@@ -113,6 +116,134 @@ class TestLazyImports:
         assert "True" in result.stdout
 
 
+def _make_godot_project(tmp_path):
+    """Create a minimal fake Godot project directory with a project.godot file."""
+    (tmp_path / "project.godot").write_text("[gd_resource]\n")
+    return tmp_path
+
+
+class TestInstallAddonCopying:
+    """Test install_addon file copying behavior."""
+
+    def test_install_addon_copies_gd_files(self, tmp_path):
+        """install_addon copies all .gd files to target/addons/godotiq/."""
+        project = _make_godot_project(tmp_path)
+        install_addon(str(project))
+        gd_files = list((project / "addons" / "godotiq").glob("*.gd"))
+        assert len(gd_files) >= 5, f"Expected >= 5 .gd files, got {gd_files}"
+
+    def test_install_addon_copies_plugin_cfg(self, tmp_path):
+        """install_addon copies plugin.cfg to target/addons/godotiq/."""
+        project = _make_godot_project(tmp_path)
+        install_addon(str(project))
+        assert (project / "addons" / "godotiq" / "plugin.cfg").exists()
+
+    def test_install_addon_copies_godotiq_rules(self, tmp_path):
+        """install_addon copies GODOTIQ_RULES.md to the target project root."""
+        project = _make_godot_project(tmp_path)
+        install_addon(str(project))
+        rules = project / "GODOTIQ_RULES.md"
+        assert rules.exists()
+        # Verify content matches source
+        src = Path(__file__).resolve().parent.parent / "src" / "godotiq" / "prompts" / "godot_development.md"
+        if src.exists():
+            assert rules.read_text() == src.read_text()
+
+    def test_install_addon_creates_addons_dir(self, tmp_path):
+        """install_addon creates addons/godotiq/ directory if it does not exist."""
+        project = _make_godot_project(tmp_path)
+        assert not (project / "addons").exists()
+        install_addon(str(project))
+        assert (project / "addons" / "godotiq").is_dir()
+
+
+class TestInstallAddonOverwrite:
+    """Test install_addon overwrite behavior."""
+
+    def test_install_addon_overwrites_existing_files(self, tmp_path, capsys):
+        """install_addon overwrites existing files and reports 'updated'."""
+        project = _make_godot_project(tmp_path)
+        addon_dir = project / "addons" / "godotiq"
+        addon_dir.mkdir(parents=True)
+        (addon_dir / "plugin.cfg").write_text("old content")
+        install_addon(str(project))
+        assert (addon_dir / "plugin.cfg").read_text() != "old content"
+        captured = capsys.readouterr()
+        assert "updated" in captured.out
+
+    def test_install_addon_reports_added_for_new_files(self, tmp_path, capsys):
+        """install_addon reports 'added' for files that did not previously exist."""
+        project = _make_godot_project(tmp_path)
+        install_addon(str(project))
+        captured = capsys.readouterr()
+        assert "added" in captured.out
+
+
+class TestInstallAddonValidation:
+    """Test install_addon validation errors."""
+
+    def test_install_addon_fails_nonexistent_dir(self):
+        """install_addon raises SystemExit for non-existent target."""
+        with pytest.raises(SystemExit):
+            install_addon("/nonexistent/path/abc123")
+
+    def test_install_addon_fails_no_project_godot(self, tmp_path):
+        """install_addon raises SystemExit when target has no project.godot."""
+        with pytest.raises(SystemExit):
+            install_addon(str(tmp_path))
+
+    def test_install_addon_resolves_relative_paths(self, tmp_path, monkeypatch):
+        """install_addon resolves relative paths correctly."""
+        (tmp_path / "myproject").mkdir()
+        project = _make_godot_project(tmp_path / "myproject")
+        monkeypatch.chdir(tmp_path)
+        install_addon("myproject")
+        assert (project / "addons" / "godotiq").is_dir()
+
+
+class TestInstallAddonDryRun:
+    """Test install_addon --dry-run behavior."""
+
+    def test_install_addon_dry_run_creates_no_files(self, tmp_path, capsys):
+        """--dry-run lists files but does not copy anything."""
+        project = _make_godot_project(tmp_path)
+        install_addon(str(project), dry_run=True)
+        assert not (project / "addons" / "godotiq").exists()
+
+    def test_install_addon_dry_run_still_validates(self, tmp_path):
+        """--dry-run still validates the target directory."""
+        with pytest.raises(SystemExit):
+            install_addon("/nonexistent/path", dry_run=True)
+
+    def test_install_addon_dry_run_lists_files(self, tmp_path, capsys):
+        """--dry-run prints the list of files that would be copied."""
+        project = _make_godot_project(tmp_path)
+        install_addon(str(project), dry_run=True)
+        captured = capsys.readouterr()
+        assert "would copy" in captured.out
+        assert "plugin.cfg" in captured.out
+
+
+class TestInstallAddonLazyImport:
+    """Verify install-addon does not import server."""
+
+    def test_install_addon_does_not_import_server(self, tmp_path):
+        """The install_addon code path does NOT import godotiq.server."""
+        project = _make_godot_project(tmp_path)
+        script = tmp_path / "check_server.py"
+        script.write_text(
+            f"import sys\n"
+            f"from godotiq.cli import install_addon\n"
+            f"install_addon('{project}')\n"
+            f"print('godotiq.server' not in sys.modules)\n"
+        )
+        result = subprocess.run(
+            [sys.executable, str(script)],
+            capture_output=True, text=True, timeout=10,
+        )
+        assert "True" in result.stdout
+
+
 class TestMainModule:
     """Test python -m godotiq routing."""
 
