diff --git a/providers/git/pyproject.toml b/providers/git/pyproject.toml
index 689f006001310..901b2cc624edb 100644
--- a/providers/git/pyproject.toml
+++ b/providers/git/pyproject.toml
@@ -64,6 +64,13 @@ dependencies = [
     "GitPython>=3.1.44",
 ]
 
+# The optional dependencies should be modified in place in the generated file
+# Any change in the dependencies is preserved when the file is regenerated
+[project.optional-dependencies]
+github = [
+    "PyGithub>=2.1.1,<3",
+]
+
 [dependency-groups]
 dev = [
     "apache-airflow",
diff --git a/providers/git/src/airflow/providers/git/hooks/git.py b/providers/git/src/airflow/providers/git/hooks/git.py
index e9e69367aa772..3ab9f6320efed 100644
--- a/providers/git/src/airflow/providers/git/hooks/git.py
+++ b/providers/git/src/airflow/providers/git/hooks/git.py
@@ -24,6 +24,7 @@
 import shlex
 import stat
 import tempfile
+from datetime import datetime, timedelta, timezone
 from typing import Any
 from urllib.parse import quote as urlquote
 
@@ -49,6 +50,10 @@ class GitHook(BaseHook):
     * ``ssh_config_file`` — path to a custom SSH config file.
     * ``host_proxy_cmd`` — SSH ProxyCommand string (e.g. for bastion/jump hosts).
     * ``ssh_port`` — non-default SSH port.
+    * ``github_app_id`` — GitHub App ID used for GitHub App authentication. Requires the GitHub App
+      private key to be provided as a PEM-encoded key via either ``private_key`` (inline) or
+      ``key_file`` (path to key file).
+    * ``github_installation_id`` — GitHub App installation ID used for GitHub App authentication.
     """
 
     conn_name_attr = "git_conn_id"
@@ -76,6 +81,8 @@ def get_ui_field_behaviour(cls) -> dict[str, Any]:
                         "ssh_config_file": "",
                         "host_proxy_cmd": "",
                         "ssh_port": "",
+                        "github_app_id": "",
+                        "github_installation_id": "",
                     }
                 )
             },
@@ -104,10 +111,36 @@ def __init__(
         self.host_proxy_cmd = extra.get("host_proxy_cmd")
         self.ssh_port: int | None = int(extra["ssh_port"]) if extra.get("ssh_port") else None
 
+        # GitHub App Auth Options
+        self.github_app_id = extra.get("github_app_id")
+        self.github_installation_id = extra.get("github_installation_id")
+        self.github_app_private_key: str | None = None
+        self.github_app_token_exp: datetime | None = None
+
         self.env: dict[str, str] = {}
 
         if self.key_file and self.private_key:
             raise AirflowException("Both 'key_file' and 'private_key' cannot be provided at the same time")
+        if (self.github_app_id is not None and self.github_installation_id is None) or (
+            self.github_app_id is None and self.github_installation_id is not None
+        ):
+            raise ValueError(
+                "Both 'github_app_id' and 'github_installation_id' must be provided to use GitHub App Authentication"
+            )
+        if self.github_app_id is not None and self.github_installation_id is not None:
+            if self.key_file and not self.private_key:
+                with open(self.key_file, encoding="utf-8") as key_file:
+                    self.private_key = key_file.read()
+            if not (self.repo_url or "").startswith(("https://", "http://")):
+                raise ValueError(
+                    f"GitHub App authentication requires an HTTPS repository URL, but got: {self.repo_url!r}"
+                )
+            # Store the PEM separately so configure_hook_env() does not treat it as an SSH key.
+            # Keep `private_key` populated for callers/tests that expect it to be available,
+            # but also keep a dedicated attribute so configure_hook_env() can avoid
+            # treating the GitHub App PEM as an SSH key.
+            self.github_app_private_key = self.private_key
+            self.auth_token = ""
         self._process_git_auth_url()
 
     _VALID_STRICT_HOST_KEY_CHECKING = frozenset({"yes", "no", "accept-new", "off", "ask"})
@@ -142,6 +175,82 @@ def _build_ssh_command(self, key_path: str | None = None) -> str:
 
         return " ".join(parts)
 
+    def _get_github_app_token(self):
+        try:
+            from github import Auth, GithubIntegration
+        except ImportError as exc:
+            raise ImportError(
+                "The PyGithub library is required for GitHub App authentication. Please install it with 'pip install apache-airflow-providers-git[github]'"
+            ) from exc
+
+        auth = Auth.AppAuth(self.github_app_id, self.github_app_private_key)
+        integration = GithubIntegration(auth=auth)
+        access_token = integration.get_access_token(installation_id=self.github_installation_id)
+        github_app_token_exp = access_token.expires_at
+        log.info(
+            "Successfully obtained GitHub App installation access token (expires at: %s)",
+            github_app_token_exp,
+        )
+
+        return "x-access-token", access_token.token, github_app_token_exp
+
+    def _ensure_github_app_token(self) -> None:
+        if self.github_app_id is None or self.github_installation_id is None:
+            return
+
+        TOKEN_REFRESH_BUFFER = timedelta(minutes=5)
+        if (
+            self.github_app_token_exp is None
+            or self.github_app_token_exp < datetime.now(timezone.utc) + TOKEN_REFRESH_BUFFER
+        ):
+            log.info(
+                "GitHub App token is missing or near expiry (expires at: %s). Refreshing token.",
+                self.github_app_token_exp,
+            )
+            self.user_name, self.auth_token, self.github_app_token_exp = self._get_github_app_token()
+
+    @contextlib.contextmanager
+    def _github_app_askpass_env(self):
+        if not self.auth_token:
+            yield
+            return
+
+        token = shlex.quote(self.auth_token)
+        with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=True) as askpass_script:
+            askpass_script.write(
+                "#!/bin/sh\n"
+                'case "$1" in\n'
+                "  *Username*) echo x-access-token;;\n"
+                f"  *Password*) echo {token};;\n"
+                f"  *) echo {token};;\n"
+                "esac\n"
+            )
+            askpass_script.flush()
+            os.chmod(askpass_script.name, stat.S_IRWXU)
+
+            old_askpass = os.environ.get("GIT_ASKPASS")
+            old_terminal_prompt = os.environ.get("GIT_TERMINAL_PROMPT")
+            try:
+                os.environ["GIT_ASKPASS"] = askpass_script.name
+                os.environ["GIT_TERMINAL_PROMPT"] = "0"
+                self.env["GIT_ASKPASS"] = askpass_script.name
+                self.env["GIT_TERMINAL_PROMPT"] = "0"
+                yield
+            finally:
+                if old_askpass is None:
+                    self.env.pop("GIT_ASKPASS", None)
+                    os.environ.pop("GIT_ASKPASS", None)
+                else:
+                    self.env["GIT_ASKPASS"] = old_askpass
+                    os.environ["GIT_ASKPASS"] = old_askpass
+
+                if old_terminal_prompt is None:
+                    self.env.pop("GIT_TERMINAL_PROMPT", None)
+                    os.environ.pop("GIT_TERMINAL_PROMPT", None)
+                else:
+                    self.env["GIT_TERMINAL_PROMPT"] = old_terminal_prompt
+                    os.environ["GIT_TERMINAL_PROMPT"] = old_terminal_prompt
+
     def _process_git_auth_url(self):
         if not isinstance(self.repo_url, str):
             return
@@ -199,7 +308,16 @@ def _passphrase_askpass_env(self):
 
     @contextlib.contextmanager
     def configure_hook_env(self):
-        if self.private_key:
+        self._ensure_github_app_token()
+
+        if self.github_app_id is not None and self.github_installation_id is not None:
+            with self._github_app_askpass_env():
+                yield
+            return
+
+        # If a GitHub App PEM is present, it should not be treated as an SSH key
+        # for configuring `GIT_SSH_COMMAND`.
+        if self.private_key and not self.github_app_private_key:
             with tempfile.NamedTemporaryFile(mode="w", delete=True) as tmp_keyfile:
                 tmp_keyfile.write(self.private_key)
                 tmp_keyfile.flush()
diff --git a/providers/git/tests/unit/git/hooks/test_git.py b/providers/git/tests/unit/git/hooks/test_git.py
index 38cc6ed5354f9..a8b819aec02e0 100644
--- a/providers/git/tests/unit/git/hooks/test_git.py
+++ b/providers/git/tests/unit/git/hooks/test_git.py
@@ -49,6 +49,12 @@ def bundle_temp_dir(tmp_path):
 CONN_ONLY_INLINE_KEY = "my_git_conn_only_inline_key"
 CONN_BOTH_PATH_INLINE = "my_git_conn_both_path_inline"
 CONN_NO_REPO_URL = "my_git_conn_no_repo_url"
+CONN_APP_INLINE_KEY = "git_app_inline_key"
+CONN_APP_ONLY_APP_ID = "git_app_only_app_id"
+CONN_APP_ONLY_INSTALLATION_ID = "git_app_only_installation_id"
+CONN_APP_NO_KEY = "git_app_no_key"
+CONN_APP_INVALID_APP_ID = "git_app_invalid_app_id"
+CONN_APP_INVALID_INSTALLATION_ID = "git_app_invalid_installation_id"
 
 
 @pytest.fixture
@@ -120,6 +126,85 @@ def setup_connections(self, create_connection_without_db):
                 },
             )
         )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_BOTH_PATH_INLINE,
+                host="path/to/repo",
+                conn_type="git",
+                extra={
+                    "key_file": "path/to/key",
+                    "private_key": "inline_key",
+                },
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id="my_git_conn_strict",
+                host=AIRFLOW_GIT,
+                conn_type="git",
+                extra='{"key_file": "/files/pkey.pem", "strict_host_key_checking": "yes"}',
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_INLINE_KEY,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "67890",
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_ONLY_APP_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={"github_app_id": "12345"},
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_ONLY_INSTALLATION_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={"github_installation_id": "67890"},
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_NO_KEY,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={"github_app_id": "12345", "github_installation_id": "67890"},
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_INVALID_APP_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "not_an_int",
+                    "github_installation_id": "67890",
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
+        create_connection_without_db(
+            Connection(
+                conn_id=CONN_APP_INVALID_INSTALLATION_ID,
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "not_an_int",
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
 
     @pytest.mark.parametrize(
         ("conn_id", "hook_kwargs", "expected_repo_url"),
@@ -352,3 +437,199 @@ def test_passphrase_askpass_cleaned_up(self, create_connection_without_db):
             assert os.path.exists(askpass_path)
         # Both the askpass script and the temp key file should be cleaned up
         assert not os.path.exists(askpass_path)
+
+    # --- GitHub App auth tests ---
+
+    def test_only_app_id_without_installation_id_raises(self):
+        with pytest.raises(
+            AirflowException, match="Both 'github_app_id' and 'github_installation_id' must be provided"
+        ):
+            GitHook(git_conn_id=CONN_APP_ONLY_APP_ID)
+
+    def test_only_installation_id_without_app_id_raises(self):
+        with pytest.raises(
+            AirflowException,
+            match="Both 'github_app_id' and 'github_installation_id' must be provided",
+        ):
+            GitHook(git_conn_id=CONN_APP_ONLY_INSTALLATION_ID)
+
+    def test_app_id_and_installation_id_without_key_raises(self):
+        with pytest.raises(
+            AirflowException,
+            match="Missing inline private_key or key_file for GitHub App Auth",
+        ):
+            GitHook(git_conn_id=CONN_APP_NO_KEY)
+
+    def test_app_auth_with_key_file_reads_file(self, create_connection_without_db, tmp_path, monkeypatch):
+        key_file = tmp_path / "app_key.pem"
+        key_file.write_text("file_pem_key_content")
+        create_connection_without_db(
+            Connection(
+                conn_id="git_app_key_file",
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "67890",
+                    "key_file": str(key_file),
+                },
+            )
+        )
+        from datetime import datetime, timedelta, timezone
+
+        mock_expiry = datetime.now(timezone.utc) + timedelta(hours=1)
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            lambda self: ("x-access-token", "ghs_test_token", mock_expiry),
+        )
+        hook = GitHook(git_conn_id="git_app_key_file")
+
+        assert hook.private_key == "file_pem_key_content"
+
+    def test_app_auth_with_missing_key_file_raises(self, create_connection_without_db):
+        create_connection_without_db(
+            Connection(
+                conn_id="git_app_missing_key_file",
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": "12345",
+                    "github_installation_id": "67890",
+                    "key_file": "/nonexistent/path/key.pem",
+                },
+            )
+        )
+        with pytest.raises(AirflowException, match="Failed to read GitHub App private key file"):
+            GitHook(git_conn_id="git_app_missing_key_file")
+
+    def test_app_auth_defers_token_fetch(self, monkeypatch):
+        """GitHub App token is not fetched in __init__, only on configure_hook_env."""
+        from datetime import datetime, timedelta, timezone
+
+        mock_called = []
+
+        def mock_get_token(self):
+            mock_called.append(True)
+            return ("x-access-token", "ghs_test_token", datetime.now(timezone.utc) + timedelta(hours=1))
+
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            mock_get_token,
+        )
+        # __init__ should NOT call _get_github_app_token
+        hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        assert len(mock_called) == 0
+        assert hook.auth_token == ""
+        assert hook.github_app_id == "12345"
+        assert hook.github_installation_id == "67890"
+
+        # First call to configure_hook_env should trigger the token fetch
+        with hook.configure_hook_env():
+            assert len(mock_called) == 1
+            assert hook.auth_token == "ghs_test_token"
+            assert hook.user_name == "x-access-token"
+
+    def test_app_auth_success_stores_app_id_and_installation_id(self):
+        """App ID and installation ID are stored at __init__ time."""
+        hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        assert hook.github_app_id == "12345"
+        assert hook.github_installation_id == "67890"
+
+    @pytest.mark.parametrize(
+        ("app_id", "installation_id"),
+        [
+            ("12345", "67890"),
+            (12345, 67890),
+        ],
+    )
+    def test_app_id_and_installation_id_are_stored_as_provided(
+        self, app_id, installation_id, create_connection_without_db, monkeypatch
+    ):
+        from datetime import datetime, timedelta, timezone
+
+        create_connection_without_db(
+            Connection(
+                conn_id="git_app_int_check",
+                host=AIRFLOW_HTTPS_URL,
+                conn_type="git",
+                extra={
+                    "github_app_id": app_id,
+                    "github_installation_id": installation_id,
+                    "private_key": "inline_pem_key",
+                },
+            )
+        )
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            lambda self: ("x-access-token", "token", datetime.now(timezone.utc) + timedelta(hours=1)),
+        )
+        hook = GitHook(git_conn_id="git_app_int_check")
+        assert hook.github_app_id == app_id
+        assert hook.github_installation_id == installation_id
+
+    def test_github_app_token_refresh_near_expiry(self, monkeypatch):
+        """Token is refreshed when near expiry during configure_hook_env."""
+        from datetime import datetime, timedelta, timezone
+
+        mock_get_token_call_count = [0]
+
+        def mock_get_token(self):
+            mock_get_token_call_count[0] += 1
+            # First call returns token expiring in 3 minutes
+            if mock_get_token_call_count[0] == 1:
+                return (
+                    "x-access-token",
+                    f"token_{mock_get_token_call_count[0]}",
+                    datetime.now(timezone.utc) + timedelta(minutes=3),
+                )
+            # Second call (refresh) returns token expiring in 1 hour
+            return (
+                "x-access-token",
+                f"token_{mock_get_token_call_count[0]}",
+                datetime.now(timezone.utc) + timedelta(hours=1),
+            )
+
+        monkeypatch.setattr(
+            "airflow.providers.git.hooks.git.GitHook._get_github_app_token",
+            mock_get_token,
+        )
+        hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        assert mock_get_token_call_count[0] == 0  # No call in __init__
+
+        # First configure_hook_env triggers first token fetch
+        with hook.configure_hook_env():
+            assert mock_get_token_call_count[0] == 1
+            assert hook.auth_token == "token_1"
+
+        # Second configure_hook_env triggers refresh (token near expiry)
+        with hook.configure_hook_env():
+            assert mock_get_token_call_count[0] == 2
+            assert hook.auth_token == "token_2"
+
+    def test_github_app_integration_call_shape(self, monkeypatch):
+        """Verify GithubIntegration is called with correct arguments."""
+        from datetime import datetime, timedelta, timezone
+        from unittest import mock
+
+        mock_integration = mock.MagicMock()
+        mock_access_token = mock.MagicMock()
+        mock_access_token.token = "ghs_test_token"
+        mock_access_token.expires_at = datetime.now(timezone.utc) + timedelta(hours=1)
+        mock_integration.get_access_token.return_value = mock_access_token
+
+        import sys
+        from types import SimpleNamespace
+
+        fake_github = SimpleNamespace(
+            Auth=SimpleNamespace(AppAuth=lambda app_id, key: "auth"),
+            GithubIntegration=lambda auth: mock_integration,
+        )
+        monkeypatch.setitem(sys.modules, "github", fake_github)
+
+        hook = GitHook(git_conn_id=CONN_APP_INLINE_KEY)
+        with hook.configure_hook_env():
+            # Verify get_access_token was called with installation_id kwarg
+            assert mock_integration.get_access_token.call_count == 1
+            _, kwargs = mock_integration.get_access_token.call_args
+            assert "installation_id" in kwargs
+            assert str(kwargs["installation_id"]) == "67890"
diff --git a/uv.lock b/uv.lock
index eca04e9cb3c27..60ffc349bffb5 100644
--- a/uv.lock
+++ b/uv.lock
@@ -5318,6 +5318,11 @@ dependencies = [
     { name = "gitpython" },
 ]
 
+[package.optional-dependencies]
+github = [
+    { name = "pygithub" },
+]
+
 [package.dev-dependencies]
 dev = [
     { name = "apache-airflow" },
@@ -5334,7 +5339,9 @@ requires-dist = [
     { name = "apache-airflow", editable = "." },
     { name = "apache-airflow-providers-common-compat", editable = "providers/common/compat" },
     { name = "gitpython", specifier = ">=3.1.44" },
+    { name = "pygithub", marker = "extra == 'github'", specifier = ">=2.1.1,<3" },
 ]
+provides-extras = ["github"]
 
 [package.metadata.requires-dev]
 dev = [
