diff --git a/ChangeLog.md b/ChangeLog.md
--- a/ChangeLog.md
+++ b/ChangeLog.md
@@ -1,5 +1,10 @@
 # Revision history for shh
 
+## 0.5.0.0 -- 2019-05-23
+
+* Change how identifiers are encoded to avoid clashes in all scenarios
+  (Potentially breaking change)
+
 ## 0.4.0.0 -- 2019-04-20
 
 * Pre-compile Shell.hs for faster loading of shh shell
diff --git a/shh.cabal b/shh.cabal
--- a/shh.cabal
+++ b/shh.cabal
@@ -1,5 +1,5 @@
 name:                shh
-version:             0.4.0.1
+version:             0.5.0.0
 synopsis:            Simple shell scripting from Haskell
 description:         Provides a shell scripting environment for Haskell. It
                      helps you use external binaries, and allows you to
diff --git a/src/Shh/Internal.hs b/src/Shh/Internal.hs
--- a/src/Shh/Internal.hs
+++ b/src/Shh/Internal.hs
@@ -20,7 +20,7 @@
 import Control.Exception as C
 import Control.Monad
 import Control.Monad.IO.Class
-import Data.Char (isLower, isSpace, isAlphaNum, isUpper, toLower, isNumber)
+import Data.Char (isLower, isSpace, isAlphaNum, ord)
 import Data.List (dropWhileEnd, intercalate)
 import Data.List.Split (endBy, splitOn)
 import qualified Data.Map as Map
@@ -43,6 +43,7 @@
 import System.IO.Error
 import System.Posix.Signals
 import System.Process
+import Text.Printf
 
 -- $setup
 -- For doc-tests. Not sure I can use TH in doc tests.
@@ -659,26 +660,35 @@
         Just absExe ->
             rawExe fnName (case ref of { Absolute -> absExe; SearchPath -> executable })
 
--- | Takes a string, and makes a Haskell identifier out of it. There
--- is some chance of overlap. If the string is a path, the filename portion
--- is used. The transformation replaces all non-alphanumeric characters
--- with @'_'@. If the first character is uppercase it is forced into lowercase.
--- If it starts with a number, it is prefixed with `_`. If it overlaps with
--- a reserved word or a builtin, it is suffixed with an `_`.
+
+-- | Takes a string, and makes a Haskell identifier out of it. If the string
+-- is a path, the filename portion is used. The exact transformation is that
+-- alphanumeric characters are unchanged, @-@ becomes @_@, and @'@ is used to
+-- escape all other characters. @_@ becomes @'_@, @.@ becomes @''@ and
+-- anthing else is becomes a hex encoded number surrounded by @'@ characters.
+--
+-- Justification for changing @-@ to @_@ is that @-@ appears far more commonly
+-- in executable names than @_@ does, and so we give it the more ergonomic
+-- encoding.
 encodeIdentifier :: String -> String
 encodeIdentifier ident =
     let
-        i = go (takeFileName ident)
-        go (c:cs)
-            | isLower  c = c : go' cs
-            | isUpper  c = toLower c : go' cs
-            | isNumber c = '_' : go' (c : cs)
-            | otherwise  = go' (c:cs)
-        go [] = "_"
-        go' (c:cs)
-            | isAlphaNum c = c : go' cs
-            | otherwise    = '_' : go' cs
-        go' [] = []
+        fixBody :: String -> String
+        fixBody (c:cs)
+            | isAlphaNum c = c : fixBody cs
+            | c == '-'     = '_' : fixBody cs
+            | c == '_'     = '\'' : '_' : fixBody cs
+            | c == '.'     = '\'' : '\'' : fixBody cs
+            | otherwise    = printf "'%x'%s" (ord c) (fixBody cs)
+        fixBody [] = []
+
+        fixStart :: String -> String
+        fixStart s@(c : _)
+            | isLower c = s
+            | otherwise = '_' : s
+        fixStart [] = []
+
+        i = fixStart $ fixBody $ takeFileName ident
         -- Includes cd, which has to be a built-in
         reserved = [ "import", "if", "else", "then", "do", "in", "let", "type"
             , "as", "case", "of", "class", "data", "default", "deriving"
@@ -686,6 +696,7 @@
             , "infixr", "mdo", "module", "newtype", "proc", "qualified"
             , "rec", "where", "cd"]
     in if i `elem` reserved then i ++ "_" else i
+
 
 -- | Scans your '$PATH' environment variable and creates a function for each
 -- executable found. Binaries that would not create valid Haskell identifiers
diff --git a/test/Test.hs b/test/Test.hs
--- a/test/Test.hs
+++ b/test/Test.hs
@@ -39,8 +39,8 @@
 properties :: TestTree
 properties = testGroup "Properties"
     [ testProperty "trim = trim . trim" $ \l -> trim l == trim (trim l)
-    , testProperty "encodeIdentifier = encodeIdentifier . encodeIdentifier"
-        $ \l -> encodeIdentifier l == encodeIdentifier (encodeIdentifier l)
+    , testProperty "encodeIdentifier creates a unique encoding"
+        $ \(l1,l2) -> (encodeIdentifier l1 == encodeIdentifier l2) == (l1 == l2)
     , testProperty "writeOutput" $ \s -> ioProperty $ do
         let
             s' = bytesToString s
@@ -137,8 +137,8 @@
     , testCase "Lazy read checks code" $ replicateM_ 30 $ do
         Left r <- catchFailure $ withRead (cat "/dev/urandom" |> false "dummy") $ pure . take 3
         r @?= Shh.Failure "false" ["dummy"] 1
-    , testCase "Identifier odd chars" $ encodeIdentifier "1@3.-" @?= "_1_3__"
-    , testCase "Identifier make lower" $ encodeIdentifier "T.est" @?= "t_est"
+    , testCase "Identifier odd chars" $ encodeIdentifier "1@3.-" @?= "_1'40'3''_"
+    , testCase "Identifier make lower" $ encodeIdentifier "T.est" @?= "_T''est"
     , testCase "pureProc closes input" $ do
         r <- readProc $ cat "/dev/urandom" |> pureProc (const "test")
         r @?= "test"
