diff --git a/git_browse/browse.py b/git_browse/browse.py
index 08673cd..80c7886 100755
--- a/git_browse/browse.py
+++ b/git_browse/browse.py
@@ -36,12 +36,21 @@ class GithubHost(object):
             self.user,
             self.repository
         )
+        if focus_object.is_commit_hash():
+            return self.commit_hash_url(repository_url, focus_object)
         if focus_object.is_root():
             return self.root_url(repository_url, focus_object)
         if focus_object.is_directory():
             return self.directory_url(repository_url, focus_object)
         return self.file_url(repository_url, focus_object)
 
+    def commit_hash_url(self, repository_url, focus_hash):
+        repository_url = "%s/commit/%s" % (
+            repository_url,
+            focus_hash.commit_hash
+        )
+        return repository_url
+
     def root_url(self, repository_url, focus_object):
         return repository_url
 
@@ -69,10 +78,13 @@ class UberPhabricatorHost(object):
         return UberPhabricatorHost(None, None)
 
     def get_url(self, focus_object):
-        path = focus_object.path
-        # arc browse requires an object, provide the root object by default
-        if focus_object.is_root():
-            path = '.'
+        if focus_object.is_commit_hash():
+            path = focus_object.commit_hash
+        else:
+            path = focus_object.path
+            # arc browse requires an object, provide the root object by default
+            if focus_object.is_root():
+                path = '.'
         command = ['arc', 'browse']
         if path:
             command.append(path)
@@ -90,6 +102,9 @@ class FocusObject(object):
     def __init__(self, path):
         self.path = path
 
+    def is_commit_hash(self):
+        return False
+
     def is_root(self):
         return self.path == os.sep
 
@@ -101,6 +116,14 @@ class FocusObject(object):
         return FocusObject(os.sep)
 
 
+class FocusHash(object):
+    def __init__(self, commit_hash):
+        self.commit_hash = commit_hash
+
+    def is_commit_hash(self):
+        return True
+
+
 def get_repository_root():
     current_directory = ''
     new_directory = os.getcwd()
@@ -166,6 +189,9 @@ def get_focus_object(sys_argv, path):
     object_path = os.path.join(directory, focus_object[0])
     object_path = os.path.normpath(object_path)
     if not os.path.exists(object_path):
+        focus_hash = get_commit_hash(focus_object[0])
+        if focus_hash:
+            return focus_hash
         error = "specified file does not exist: %s" % object_path
         raise FileNotFoundError(error)
     is_dir = os.path.isdir(object_path) and object_path[-1] != os.sep
@@ -175,6 +201,21 @@ def get_focus_object(sys_argv, path):
     return FocusObject(object_path)
 
 
+def get_commit_hash(focus_object):
+    command = ['git', 'show', focus_object]
+    process = subprocess.Popen(
+        command,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+        universal_newlines=True
+    )
+    out, err = process.communicate()
+    if process.returncode != 0:
+        return None
+    commit_hash = out.split("\n")[0].split(" ")[1]
+    return FocusHash(commit_hash)
+
+
 def open_url(url):
     print(url)
     if url.__class__ is list:

Support git browsing a commit hash
diff --git a/git_browse/tests/test.py b/git_browse/tests/test.py
index a96ba45..75ae39b 100644
--- a/git_browse/tests/test.py
+++ b/git_browse/tests/test.py
@@ -15,6 +15,7 @@ class TestGithubHost(unittest.TestCase):
         self.github_host = browse.GithubHost('albertyw', 'git-browse')
         self.repository_url = 'https://github.com/albertyw/git-browse'
         self.focus_object = browse.FocusObject('/')
+        self.focus_hash = browse.FocusHash('v2.0.0')
 
     def test_init(self):
         host = browse.GithubHost('user', 'repository')
@@ -48,6 +49,16 @@ class TestGithubHost(unittest.TestCase):
             'https://github.com/albertyw/git-browse/blob/master/README.md'
         )
 
+    def test_commit_hash_url(self):
+        url = self.github_host.commit_hash_url(
+            self.repository_url,
+            self.focus_hash
+        )
+        self.assertEqual(
+            url,
+            'https://github.com/albertyw/git-browse/commit/v2.0.0'
+        )
+
 
 class FocusObject(unittest.TestCase):
     def test_init(self):
@@ -75,6 +86,16 @@ class FocusObject(unittest.TestCase):
         self.assertTrue(obj.is_root())
 
 
+class FocusHash(unittest.TestCase):
+    def test_init(self):
+        obj = browse.FocusHash('abcde')
+        self.assertEqual(obj.commit_hash, 'abcde')
+
+    def test_is_commit_hash(self):
+        obj = browse.FocusHash('abcde')
+        self.assertTrue(obj.is_commit_hash())
+
+
 class GetRepositoryRoot(unittest.TestCase):
     def test_get(self):
         os.chdir(BASE_DIRECTORY)
@@ -179,12 +200,30 @@ class TestGetFocusObject(unittest.TestCase):
         self.assertFalse(focus_object.is_root())
         self.assertTrue(focus_object.is_directory())
 
+    def test_get_focus_hash(self):
+        sys_argv = ['asdf', 'v2.0.0']
+        focus_object = browse.get_focus_object(sys_argv, os.getcwd())
+        self.assertTrue(focus_object.__class__ is browse.FocusHash)
+
     def test_nonexistend_focus_object(self):
         sys_argv = ['asdf', 'asdf']
         with self.assertRaises(FileNotFoundError):
             browse.get_focus_object(sys_argv, os.getcwd())
 
 
+class TestGetCommitHash(unittest.TestCase):
+    def test_get_unknown_hash(self):
+        focus_object = '!@#$'
+        focus_hash = browse.get_commit_hash(focus_object)
+        self.assertEqual(focus_hash, None)
+
+    def test_get_hash(self):
+        focus_object = 'v2.0.0'
+        focus_hash = browse.get_commit_hash(focus_object)
+        self.assertTrue(focus_hash.__class__ is browse.FocusHash)
+        self.assertTrue(focus_hash.commit_hash)
+
+
 class TestOpenURL(unittest.TestCase):
     @patch("builtins.print", autospec=True)
     def test_open_url(self, mock_print):
diff --git a/git_browse/tests/test_git_urls.py b/git_browse/tests/test_git_urls.py
index e570b67..75d62e3 100644
--- a/git_browse/tests/test_git_urls.py
+++ b/git_browse/tests/test_git_urls.py
@@ -49,6 +49,12 @@ GIT_URLS = [
         TEST_DIR,
         'https://github.com/albertyw/git-browse/tree/master/testdir/'
     ),
+    (
+        'git@github.com:albertyw/git-browse',
+        'v2.0.0',
+        'https://github.com/albertyw/git-browse/commit/' +
+        'f5631b4c423f2fa5c9c4b64853607f1727d4b7a9'
+    ),
     (
         'gitolite@code.uber.internal:a/b',
         None,
@@ -63,6 +69,11 @@ GIT_URLS = [
         'gitolite@code.uber.internal:a/b',
         TEST_DIR,
         ['arc', 'browse', TEST_DIR+'/']
+    ),
+    (
+        'gitolite@code.uber.internal:a/b',
+        'v2.0.0',
+        ['arc', 'browse', 'f5631b4c423f2fa5c9c4b64853607f1727d4b7a9']
     )
 ]
 

pip install -e .
parse_log_pytest
pytest flake8
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
exceptiongroup==1.2.2
flake8==7.2.0
-e git+https://github.com/albertyw/git-browse.git@9cd501dba9d2d65a6a25265023a1befd253a620c#egg=git_browse
iniconfig==2.1.0
mccabe==0.7.0
packaging==24.2
pluggy==1.5.0
pycodestyle==2.13.0
pyflakes==3.3.2
pytest==8.3.5
tomli==2.2.1

name: git-browse
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - iniconfig==2.1.0
      - mccabe==0.7.0
      - packaging==24.2
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pyflakes==3.3.2
      - pytest==8.3.5
      - tomli==2.2.1
prefix: /opt/conda/envs/git-browse

git_browse/tests/test.py::TestGithubHost::test_commit_hash_url git_browse/tests/test.py::TestGithubHost::test_directory_url git_browse/tests/test.py::TestGithubHost::test_file_url git_browse/tests/test.py::TestGithubHost::test_get_url git_browse/tests/test.py::TestGithubHost::test_init git_browse/tests/test.py::TestGithubHost::test_root_url git_browse/tests/test.py::FocusHash::test_init git_browse/tests/test.py::FocusHash::test_is_commit_hash git_browse/tests/test.py::TestGetFocusObject::test_get_focus_hash git_browse/tests/test.py::TestGetCommitHash::test_get_hash git_browse/tests/test.py::TestGetCommitHash::test_get_unknown_hash git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_12 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_8
git_browse/tests/test.py::GetGitURL::test_url
git_browse/tests/test.py::FocusObject::test_default git_browse/tests/test.py::FocusObject::test_init git_browse/tests/test.py::FocusObject::test_is_directory git_browse/tests/test.py::FocusObject::test_is_not_directory git_browse/tests/test.py::FocusObject::test_is_not_root git_browse/tests/test.py::FocusObject::test_is_root git_browse/tests/test.py::GetRepositoryRoot::test_fail_get git_browse/tests/test.py::GetRepositoryRoot::test_get git_browse/tests/test.py::GetGitConfig::test_get git_browse/tests/test.py::GetGitURL::test_bad_url git_browse/tests/test.py::ParseGitURL::test_broken_url git_browse/tests/test.py::ParseGitURL::test_https_url git_browse/tests/test.py::ParseGitURL::test_ssh_url git_browse/tests/test.py::TestGetRepositoryHost::test_repository_host git_browse/tests/test.py::TestGetFocusObjectPath::test_get_cwd git_browse/tests/test.py::TestGetFocusObjectPath::test_get_path_override git_browse/tests/test.py::TestGetFocusObject::test_default_focus_object git_browse/tests/test.py::TestGetFocusObject::test_directory_focus_object git_browse/tests/test.py::TestGetFocusObject::test_file_focus_object git_browse/tests/test.py::TestGetFocusObject::test_nonexistend_focus_object git_browse/tests/test.py::TestOpenURL::test_open_url git_browse/tests/test.py::FullTest::test_chdir_subdirectory git_browse/tests/test.py::FullTest::test_chdir_subdirectory_file git_browse/tests/test.py::FullTest::test_default git_browse/tests/test.py::FullTest::test_directory git_browse/tests/test.py::FullTest::test_file git_browse/tests/test.py::FullTest::test_subdirectory git_browse/tests/test.py::FullTest::test_subdirectory_file git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_0 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_1 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_10 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_11 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_2 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_3 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_4 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_5 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_6 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_7 git_browse/tests/test_git_urls.py::TestGitURLs::test_git_url_9
git_browse/browse.py
git_browse/browse.py