diff --git a/reviewboard/hostingsvcs/github.py b/reviewboard/hostingsvcs/github.py
index d6d7476c66c541aca41c28521c42c92a070c756b..bcf8d46195660a6e5ae53fc94e63973eb54abf3d 100644
--- a/reviewboard/hostingsvcs/github.py
+++ b/reviewboard/hostingsvcs/github.py
@@ -12,13 +12,16 @@ from django.conf.urls import patterns, url
 from django.contrib.sites.models import Site
 from django.core.cache import cache
 from django.http import HttpResponse, HttpResponseBadRequest
+from django.template import RequestContext
+from django.template.loader import render_to_string
 from django.utils import six
 from django.utils.six.moves.urllib.error import HTTPError, URLError
+from django.utils.six.moves.urllib.parse import urljoin
 from django.utils.translation import ugettext_lazy as _
 from django.views.decorators.http import require_POST
 from djblets.siteconfig.models import SiteConfiguration
 
-from reviewboard.admin.server import get_server_url
+from reviewboard.admin.server import build_server_url, get_server_url
 from reviewboard.hostingsvcs.errors import (AuthorizationError,
                                             HostingServiceError,
                                             InvalidPlanError,
@@ -265,6 +268,8 @@ class GitHub(HostingService):
     supports_two_factor_auth = True
     supported_scmtools = ['Git']
 
+    has_repository_hook_instructions = True
+
     client_class = GitHubClient
 
     repository_url_patterns = patterns(
@@ -669,6 +674,33 @@ class GitHub(HostingService):
         return Commit(author_name, revision, date, message, parent_revision,
                       diff=diff)
 
+    def get_repository_hook_instructions(self, request, repository):
+        """Returns instructions for setting up incoming webhooks."""
+        plan = repository.extra_data['repository_plan']
+        add_webhook_url = urljoin(
+            self.account.hosting_url or 'https://github.com/',
+            '%s/%s/settings/hooks/new'
+            % (self._get_repository_owner_raw(plan, repository.extra_data),
+               self._get_repository_name_raw(plan, repository.extra_data)))
+
+        webhook_endpoint_url = build_server_url(local_site_reverse(
+            'github-hooks-close-submitted',
+            local_site=repository.local_site,
+            kwargs={
+                'repository_id': repository.pk,
+                'hosting_service_id': repository.hosting_account.service_name,
+            }))
+
+        return render_to_string(
+            'hostingsvcs/github/repo_hook_instructions.html',
+            RequestContext(request, {
+                'repository': repository,
+                'server_url': get_server_url(),
+                'add_webhook_url': add_webhook_url,
+                'webhook_endpoint_url': webhook_endpoint_url,
+                'hook_uuid': repository.get_or_create_hooks_uuid(),
+            }))
+
     def _reset_authorization(self, client_id, client_secret, token):
         """Resets the authorization info for an OAuth app-linked token.
 
diff --git a/reviewboard/hostingsvcs/service.py b/reviewboard/hostingsvcs/service.py
index fa0f6260946e4d01339dd8e6aaadf3190b2d3dc5..7514f8c8d5ec677480fd7acc44495ecfa8dc21d4 100644
--- a/reviewboard/hostingsvcs/service.py
+++ b/reviewboard/hostingsvcs/service.py
@@ -170,6 +170,7 @@ class HostingService(object):
     supports_repositories = False
     supports_ssh_key_association = False
     supports_two_factor_auth = False
+    has_repository_hook_instructions = False
     self_hosted = False
     repository_url_patterns = None
 
@@ -332,6 +333,19 @@ class HostingService(object):
 
         return results
 
+    def get_repository_hook_instructions(self, request, repository):
+        """Returns instructions for setting up incoming webhooks.
+
+        Subclasses can override this (and set
+        `has_repository_hook_instructions = True` on the subclass) to provide
+        instructions that administrators can see when trying to configure an
+        incoming webhook for the hosting service.
+
+        This is expected to return HTML for the instructions. The function
+        is responsible for escaping any content.
+        """
+        raise NotImplementedError
+
     @classmethod
     def get_bug_tracker_requires_username(cls, plan=None):
         if not cls.supports_bug_trackers:
diff --git a/reviewboard/scmtools/admin.py b/reviewboard/scmtools/admin.py
index 3d0732481155366bbf6a093a9458c9007bebf304..4823fde66f353eb1458f21cc5e08e94acff2dcf4 100644
--- a/reviewboard/scmtools/admin.py
+++ b/reviewboard/scmtools/admin.py
@@ -3,8 +3,10 @@ from __future__ import unicode_literals
 from django.contrib import admin
 from django.db.models.signals import pre_delete
 from django.dispatch import receiver
+from django.http import HttpResponse, HttpResponseNotFound
 from django.shortcuts import get_object_or_404, render_to_response
 from django.template import RequestContext
+from django.utils.html import format_html
 from django.utils.translation import ugettext_lazy as _
 
 from reviewboard.accounts.admin import fix_review_counts
@@ -15,6 +17,7 @@ from reviewboard.scmtools.models import Repository, Tool
 
 class RepositoryAdmin(admin.ModelAdmin):
     list_display = ('__str__', 'path', 'hosting', '_visible', 'inline_actions')
+    list_select_related = ('hosting_account',)
     raw_id_fields = ('local_site',)
     fieldsets = (
         (_('General Information'), {
@@ -89,14 +92,25 @@ class RepositoryAdmin(admin.ModelAdmin):
         return ''
 
     def inline_actions(self, repository):
-        return ('<div class="admin-inline-actions">'
-                ' <a class="action-rbtools-setup"'
-                '    href="%(id)s/rbtools-setup/">[%(rbtools_setup)s]</a>'
-                '</div>'
-                % {
-                    'id': repository.pk,
-                    'rbtools_setup': _('RBTools Setup'),
-                })
+        s = ['<div class="admin-inline-actions">']
+
+        if repository.hosting_account:
+            service = repository.hosting_account.service
+
+            if service and service.has_repository_hook_instructions:
+                s.append(format_html(
+                    '<a class="action-hooks-setup"'
+                    '   href="{0}/hooks-setup/">[{1}]</a>',
+                    repository.pk, _('Hooks')))
+
+        s.append(format_html(
+            '<a class="action-rbtools-setup"'
+            '   href="{0}/rbtools-setup/">[{1}]</a>',
+            repository.pk, _('RBTools Setup')))
+
+        s.append('</div>')
+
+        return ''.join(s)
     inline_actions.allow_tags = True
     inline_actions.short_description = ''
 
@@ -111,10 +125,25 @@ class RepositoryAdmin(admin.ModelAdmin):
         return patterns(
             '',
 
+            (r'^(?P<repository_id>[0-9]+)/hooks-setup/$',
+             self.admin_site.admin_view(self.hooks_setup)),
+
             (r'^(?P<repository_id>[0-9]+)/rbtools-setup/$',
              self.admin_site.admin_view(self.rbtools_setup)),
         ) + super(RepositoryAdmin, self).get_urls()
 
+    def hooks_setup(self, request, repository_id):
+        repository = get_object_or_404(Repository, pk=repository_id)
+
+        if repository.hosting_account:
+            service = repository.hosting_account.service
+
+            if service and service.has_repository_hook_instructions:
+                return HttpResponse(service.get_repository_hook_instructions(
+                    request, repository))
+
+        return HttpResponseNotFound()
+
     def rbtools_setup(self, request, repository_id):
         repository = get_object_or_404(Repository, pk=repository_id)
 
diff --git a/reviewboard/static/rb/css/admin.less b/reviewboard/static/rb/css/admin.less
index 414513c13ad30706e6e055057015ad341b119223..e6195d69e879848b1ffc3fc03d31beabee9cda4b 100644
--- a/reviewboard/static/rb/css/admin.less
+++ b/reviewboard/static/rb/css/admin.less
@@ -907,6 +907,7 @@ td, th {
           }
 
           .admin-inline-actions {
+            text-align: right;
             white-space: nowrap;
 
             a {
@@ -1138,9 +1139,15 @@ table#change-history {
  * Modal boxes
  ****************************************************************************/
 .modalbox-contents {
+  padding: 1em;
+  max-width: 70em;
+
   p, pre {
     color: black;
-    font-size: 12px;
+
+    &:first-child {
+      margin-top: 0;
+    }
 
     a {
       color: blue;
@@ -1148,9 +1155,21 @@ table#change-history {
     }
   }
 
+  p, pre, th, td {
+    font-size: 12px;
+  }
+
   pre {
     margin: 2em 1em;
   }
+
+  table.sample-fields {
+    margin-left: 1em;
+
+    th {
+      text-align: right;
+    }
+  }
 }
 
 
diff --git a/reviewboard/templates/admin/scmtools/repository/change_list.html b/reviewboard/templates/admin/scmtools/repository/change_list.html
index 8686ba67a743b916c4545ffa0b495938c7425570..a7694fb05bb54f9e2ebf1bf2b67b1246656183d5 100644
--- a/reviewboard/templates/admin/scmtools/repository/change_list.html
+++ b/reviewboard/templates/admin/scmtools/repository/change_list.html
@@ -5,17 +5,24 @@
 
 <script>
 function showInstructionsBox(url, title) {
-    $('<div>')
-        .load(url)
-        .modalBox({
-            title: title
+    var $el = $('<div>')
+        .load(url, function() {
+            $el.modalBox({
+                title: title
+            });
         });
 }
 
-$(document).on('click', '.action-rbtools-setup', function(evt) {
-    evt.preventDefault();
-    showInstructionsBox(evt.target.href,
-                        gettext('RBTools Setup Instructions'));
-});
+$(document)
+    .on('click', '.action-hooks-setup', function(evt) {
+        evt.preventDefault();
+        showInstructionsBox(evt.target.href,
+                            gettext('Hooks Setup Instructions'));
+    })
+    .on('click', '.action-rbtools-setup', function(evt) {
+        evt.preventDefault();
+        showInstructionsBox(evt.target.href,
+                            gettext('RBTools Setup Instructions'));
+    });
 </script>
 {% endblock %}
diff --git a/reviewboard/templates/hostingsvcs/github/repo_hook_instructions.html b/reviewboard/templates/hostingsvcs/github/repo_hook_instructions.html
new file mode 100644
index 0000000000000000000000000000000000000000..efd181806da190ebb00da1c9bc289edbba561093
--- /dev/null
+++ b/reviewboard/templates/hostingsvcs/github/repo_hook_instructions.html
@@ -0,0 +1,40 @@
+{% load i18n %}
+
+<p>
+{% blocktrans %}
+ Review Board supports closing review requests that are referenced in
+ commit messages for any changes pushed to this repository. These references
+ are in the form of:
+{% endblocktrans %}
+</p>
+<pre>Reviewed at {{server_url}}r/123/</pre>
+<p>
+ Or:
+</p>
+<pre>Review Request #123</pre>
+<p>
+{% blocktrans %}
+ To configure this,
+ <a href="{{add_webhook_url}}" target="_blank">add a new webhook</a>
+ for your GitHub repository and set:
+{% endblocktrans %}
+</p>
+<table class="sample-fields">
+ <tr>
+  <th>Payload URL:</th>
+  <td><tt>{{webhook_endpoint_url}}</tt></td>
+ </tr>
+ <tr>
+  <th>Content type:</th>
+  <td><tt>application/json</tt></td>
+ </tr>
+ <tr>
+  <th>Secret:</th>
+  <td><tt>{{hook_uuid}}</tt></td>
+ </tr>
+</table>
+<p>
+{% blocktrans %}
+ Then click <b>Add webhook</b>. You're done!
+{% endblocktrans %}
+</p>
