diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Revision history for pms-infrastructure
 
+## 0.0.3.0 -- 2025-06-22
+
+* Added file change monitoring for tools-list.json.
+
 ## 0.0.2.0 -- 2025-06-15
 
 * Added finally to expect to ensure the lock is released.
diff --git a/pms-infrastructure.cabal b/pms-infrastructure.cabal
--- a/pms-infrastructure.cabal
+++ b/pms-infrastructure.cabal
@@ -20,7 +20,7 @@
 -- PVP summary:     +-+------- breaking API changes
 --                  | | +----- non-breaking API additions
 --                  | | | +--- code changes with no API change
-version:            0.0.2.0
+version:            0.0.3.0
 
 -- A short (one-line) description of the package.
 synopsis:           pms-infrastructure
@@ -57,7 +57,7 @@
 
 library
     -- Import common warning flags.
-    ghc-options: -Wall
+    ghc-options: -Wall -threaded
 
     -- Modules exported by the library.
     exposed-modules:  PMS.Infrastructure.DM.Type,
@@ -92,6 +92,7 @@
                       posix-pty,
                       directory,
                       hie-bios,
+                      strip-ansi-escape,
                       pms-domain-model
 
     -- Directories containing source files.
@@ -103,7 +104,7 @@
 
 test-suite pms-infrastructure-test
     -- Import common warning flags.
-    ghc-options: -Wall
+    ghc-options: -Wall -threaded
 
     -- Base language which the package is written in.
     default-language: GHC2021
diff --git a/src/PMS/Infrastructure/DS/Core.hs b/src/PMS/Infrastructure/DS/Core.hs
--- a/src/PMS/Infrastructure/DS/Core.hs
+++ b/src/PMS/Infrastructure/DS/Core.hs
@@ -11,6 +11,7 @@
 import Control.Monad.Reader
 import qualified Control.Concurrent.STM as STM
 import Data.Conduit
+import qualified Control.Concurrent as CC
 import Control.Concurrent.Async
 import qualified Data.Text as T
 import Control.Monad.Except
@@ -23,6 +24,9 @@
 import qualified Data.Text.Encoding as TE
 import qualified Data.ByteString.Char8 as BS
 import qualified Data.ByteString.Lazy.Char8 as BL
+import qualified Data.String.AnsiEscapeCodes.Strip.Text as ANSI
+import qualified Data.Text.Encoding.Error as TEE
+import Control.Monad
 
 import qualified HIE.Bios as HIE
 import qualified HIE.Bios.Types as HIE
@@ -77,10 +81,11 @@
       cmd2task
 
     go :: DM.Command -> AppContext (IOTask ())
+    go (DM.EchoCommand dat) = genEchoTask dat
     go (DM.PtyConnectCommand dat) = genPtyConnectTask dat
+    go (DM.PtyTerminateCommand dat) = genPtyTerminateTask dat
     go (DM.PtyMessageCommand dat) = genPtyMessageTask dat
     go (DM.SystemCommand dat) = genSystemTask dat
-    go (DM.EchoCommand dat) = genEchoTask dat
 
 ---------------------------------------------------------------------------------
 -- |
@@ -132,16 +137,17 @@
 --
 genSystemTask :: DM.SystemCommandData -> AppContext (IOTask ())
 genSystemTask dat = do
-  scriptsDir <- view DM.scriptsDirDomainData <$> lift ask
+  toolsDir <- view DM.toolsDirDomainData <$> lift ask
 
-  let name = dat^.DM.nameSystemCommandData
+  let nameTmp = dat^.DM.nameSystemCommandData
       callback = dat^.DM.callbackSystemCommandData
       argsBS = DM.unRawJsonByteString $ dat^.DM.argumentsSystemCommandData
-  
   args <- liftEither $ eitherDecode $ argsBS
+  
+  name <- validateCommand nameTmp
+  argsStr <- validateCommandArg $ args^.argumentsStringToolParams
 
-  -- ToDo: validation
-  let cmd = scriptsDir </> name ++ ".sh" ++ " " ++ (args^.argumentsStringToolParams)
+  let cmd = toolsDir </> name ++ ".sh" ++ " " ++ argsStr
 
   $logDebugS DM._LOGTAG $ T.pack $ "systemTask: system cmd. " ++ cmd
   return $ systemTask cmd callback
@@ -167,17 +173,20 @@
   let name     = dat^.DM.namePtyConnectCommandData
       callback = dat^.DM.callbackPtyConnectCommandData
       argsBS   = DM.unRawJsonByteString $ dat^.DM.argumentsPtyConnectCommandData
+      tout     = 30 * 1000 * 1000
 
   prompts <- view DM.promptsDomainData <$> lift ask
   pmsTMVar  <- view pmsAppData <$> ask
   procTMVar <- view processHandleAppData <$> ask
   lockTMVar <- view lockAppData <$> ask
 
-  (cmd, argsArray)  <- getCommandArgs name argsBS
-  
+  (cmdTmp, argsArrayTmp)  <- getCommandArgs name argsBS
+  cmd <- validateCommand cmdTmp
+  argsArray <- validateCommandArgs argsArrayTmp
+
   $logDebugS DM._LOGTAG $ T.pack $ "ptyConnectTask: cmd. " ++ cmd ++ " " ++ show argsArray
 
-  return $ ptyConnectTask pmsTMVar procTMVar lockTMVar cmd argsArray prompts callback
+  return $ ptyConnectTask pmsTMVar procTMVar lockTMVar cmd argsArray prompts tout callback
 
   where
     -- |
@@ -238,12 +247,14 @@
                -> String
                -> [String]
                -> [String]
+               -> Int
                -> DM.PtyConnectCommandCallback ()
                -> IOTask ()
-ptyConnectTask pmsTMVar procTMVar lockTMVar cmd args prompts callback = flip E.catchAny errHdl $ do
+ptyConnectTask pmsTMVar procTMVar lockTMVar cmd args prompts tout callback = flip E.catchAny errHdl $ do
   hPutStrLn stderr $ "[INFO] PMS.Infrastructure.DS.Core.work.ptyConnectTask run. " ++ cmd ++ " " ++ show args
 
   let env = Nothing
+      -- env = Just [("TERM", "dumb")]
       dim = (80, 24)
 
   (pms, procHdl) <- spawnWithPty env True cmd args dim
@@ -260,9 +271,9 @@
       callback (ExitFailure 1) "" "Process is already connected."
     Nothing -> STM.atomically $ STM.putTMVar procTMVar (Just procHdl)
 
-  res <- expect lockTMVar pms prompts
-
-  callback ExitSuccess (maybe "Nothing" id res) ""
+  race (expect lockTMVar pms prompts) (CC.threadDelay tout) >>= \case
+    Left res  -> callback ExitSuccess (maybe "Nothing" id res) ""
+    Right _ -> E.throwString "timeout occurred."
 
   hPutStrLn stderr "[INFO] PMS.Infrastructure.DS.Core.work.ptyConnectTask end."
 
@@ -275,11 +286,56 @@
 
 -- |
 --
+genPtyTerminateTask :: DM.PtyTerminateCommandData -> AppContext (IOTask ())
+genPtyTerminateTask dat = do
+  let callback = dat^.DM.callbackPtyTerminateCommandData
+  
+  pmsTMVar  <- view pmsAppData <$> ask
+  procTMVar <- view processHandleAppData <$> ask
+
+  $logDebugS DM._LOGTAG $ T.pack $ "ptyTerminateTask called. "
+  return $ ptyTerminateTask pmsTMVar procTMVar callback
+
+-- |
+--
+ptyTerminateTask :: STM.TMVar (Maybe Pty)
+                 -> STM.TMVar (Maybe ProcessHandle)
+                 -> DM.PtyTerminateCommandCallback ()
+                 -> IOTask ()
+ptyTerminateTask pmsTMVar procTMVar callback = flip E.catchAny errHdl $ do
+  hPutStrLn stderr $ "[INFO] PMS.Infrastructure.DS.Core.work.ptyTerminateTask run. "
+
+  STM.atomically (STM.swapTMVar pmsTMVar Nothing) >>= \case
+    Nothing -> do
+      hPutStrLn stderr "[ERROR] PMS.Infrastructure.DS.Core.work.ptyTerminateTask: pty is not connected."
+      callback (ExitFailure 1) "" "PTY is not connected."
+    Just pms -> STM.atomically (STM.swapTMVar procTMVar Nothing) >>= \case
+      Nothing -> do
+        hPutStrLn stderr "[ERROR] PMS.Infrastructure.DS.Core.work.ptyTerminateTask: invalid pty status."
+        callback (ExitFailure 1) "" "invalid pty status."
+      Just phandle -> do
+        hPutStrLn stderr $ "[INFO] PMS.Infrastructure.DS.Core.work.ptyTerminateTask closePty : "
+        closePty pms
+        exitCode <- waitForProcess phandle
+        callback exitCode "pty teminated." ""
+
+  hPutStrLn stderr "[INFO] PMS.Infrastructure.DS.Core.work.ptyTerminateTask end."
+
+  where
+    -- |
+    --
+    errHdl :: E.SomeException -> IO ()
+    errHdl e = callback (ExitFailure 1) "" (show e)
+
+
+
+-- |
+--
 genPtyMessageTask :: DM.PtyMessageCommandData -> AppContext (IOTask ())
 genPtyMessageTask dat = do
   let callback = dat^.DM.callbackPtyMessageCommandData
       argsBS = DM.unRawJsonByteString $ dat^.DM.argumentsPtyMessageCommandData
-  
+      tout = 30 * 1000 * 1000 
   prompts <- view DM.promptsDomainData <$> lift ask
   pmsTMVar  <- view pmsAppData <$> ask
   procTMVar <- view processHandleAppData <$> ask
@@ -288,7 +344,7 @@
   let args = argsDat^.argumentsStringToolParams
 
   $logDebugS DM._LOGTAG $ T.pack $ "ptyMessageTask: args. " ++ args
-  return $ ptyMessageTask pmsTMVar procTMVar lockTMVar args prompts callback
+  return $ ptyMessageTask pmsTMVar procTMVar lockTMVar args prompts tout callback
 
 -- |
 --
@@ -296,20 +352,18 @@
                -> STM.TMVar (Maybe ProcessHandle)
                -> STM.TMVar ()
                -> String  -- arguments line
-               -> [String]  -- promt list
+               -> [String]  -- prompt list
+               -> Int       -- timeout microsec
                -> DM.PtyMessageCommandCallback ()
                -> IOTask ()
-ptyMessageTask pmsTMVar _ lockTMVar args prompts callback = flip E.catchAny errHdl $ do
+ptyMessageTask ptyTMVar _ lockTMVar args prompts tout callback = flip E.catchAny errHdl $ do
   hPutStrLn stderr $ "[INFO] PMS.Infrastructure.DS.Core.work.ptyMessageTask run. " ++ args
 
-  STM.atomically (STM.readTMVar pmsTMVar) >>= \case
+  STM.atomically (STM.readTMVar ptyTMVar) >>= \case
     Nothing -> do
-      hPutStrLn stderr "[ERROR] PMS.Infrastructure.DS.Core.work.ptyMessageTask: pms is not connected."
+      hPutStrLn stderr "[ERROR] PMS.Infrastructure.DS.Core.work.ptyMessageTask: pty is not connected."
       callback (ExitFailure 1) "" "PTY is not connected."
-    Just pms -> do
-      writePty pms $ TE.encodeUtf8 $ T.pack $ args ++ _LF
-      res <- expect lockTMVar pms prompts
-      callback ExitSuccess (maybe "Nothing" id res) ""
+    Just pty -> go pty
 
   hPutStrLn stderr "[INFO] PMS.Infrastructure.DS.Core.work.ptyMessageTask end."
 
@@ -319,6 +373,19 @@
     errHdl :: E.SomeException -> IO ()
     errHdl e = callback (ExitFailure 1) "" (show e)
 
+    go :: Pty -> IO ()
+    go pty = do
+      msg <- validateMessage args
+      let cmd = TE.encodeUtf8 $ T.pack $ msg ++ _LF
+      hPutStrLn stderr $ "[INFO] PMS.Infrastructure.DS.Core.work.ptyMessageTask writePty : " ++ BS.unpack cmd
+      writePty pty cmd
+      
+      race (expect lockTMVar pty prompts) (CC.threadDelay tout) >>= \case
+        Left res  -> callback ExitSuccess (maybe "Nothing" id res) ""
+        Right _ -> E.throwString "timeout occurred."
+
+
+
 -- |
 --
 expect :: STM.TMVar () -> Pty -> [String] -> IO (Maybe String)
@@ -345,7 +412,6 @@
     finalize :: IO ()
     finalize = STM.atomically $ STM.putTMVar lock ()
 
-
 -- |
 --
 readUntilPrompt :: Pty -> [String] -> IO BS.ByteString
@@ -357,12 +423,41 @@
 
     go acc = do
       chunk <- readPty pms
-      hPutStrLn stderr $ "[INFO] chunk:\n" ++ T.unpack (TE.decodeUtf8 chunk)
+      when ("\ESC[6n" `BS.isInfixOf` chunk) $ do
+        E.throwString "Unsupported: Detected cursor position report request (ESC[6n)."
+
+      let txt = ANSI.stripAnsiEscapeCodes $ TE.decodeUtf8With TEE.lenientDecode chunk
+      hPutStrLn stderr $ "[INFO] chunk:\n" ++ T.unpack txt
+
       let acc' = BS.append acc chunk
       if foundPrompt acc'
         then return acc'
         else go acc'
 
+{-
+readUntilPrompt :: Pty -> [String] -> IO BS.ByteString
+readUntilPrompt pms prompts = go BS.empty T.empty
+  where
+    promptTextList = map T.pack prompts
+
+    foundPrompt :: T.Text -> Bool
+    foundPrompt txt = any (`T.isInfixOf` txt) promptTextList
+
+    go accBs accTxt = do
+      chunk <- readPty pms
+      let txt = ANSI.stripAnsiEscapeCodes $ TE.decodeUtf8With TEE.lenientDecode chunk
+      hPutStrLn stderr $ "[INFO] chunk:\n" ++ T.unpack txt
+
+      when ("\ESC[6n" `BS.isInfixOf` chunk) $ do
+        hPutStrLn stderr "[INFO] Detected cursor position report request, replying with ESC[1;1R"
+        writePty pms (DBS.pack ([0x1B :: Word8, 0x5B :: Word8] ++ map (fromIntegral . ord) "0;0R"))
+
+      let accBs' = BS.append accBs chunk
+          accTxt' = T.append accTxt txt
+      if foundPrompt accTxt'
+        then return accBs'
+        else go accBs' accTxt'
+-}
 
 -- |
 -- 
diff --git a/src/PMS/Infrastructure/DS/Utility.hs b/src/PMS/Infrastructure/DS/Utility.hs
--- a/src/PMS/Infrastructure/DS/Utility.hs
+++ b/src/PMS/Infrastructure/DS/Utility.hs
@@ -8,6 +8,9 @@
 import Control.Monad.Except
 import Control.Monad.Reader
 import System.Exit
+import qualified Data.Text as T
+import Data.Char
+import Control.Monad
 
 import qualified PMS.Domain.Model.DM.Type as DM
 import qualified PMS.Domain.Model.DS.Utility as DM
@@ -39,3 +42,70 @@
 exit2int :: ExitCode -> Int
 exit2int ExitSuccess = 0
 exit2int (ExitFailure n) = n
+
+
+-- |
+--
+validateCommand :: String -> AppContext String
+validateCommand cmd = do
+  when (null cmd) $
+    throwError "Command is empty."
+
+  when (".." `T.isInfixOf` tcmd) $
+    throwError "Command contains directory traversal '..'."
+
+  when ("/" `T.isInfixOf` tcmd) $
+    throwError "Command must not contain '/'."
+
+  when ("\\" `T.isInfixOf` tcmd) $
+    throwError "Command must not contain '\\'."
+
+  when (any (not . isAllowedChar) cmd) $
+    throwError $ "Command contains disallowed characters: " ++ cmd
+
+  return cmd
+
+  where
+    tcmd = T.pack cmd
+    isAllowedChar c = isAlphaNum c || c `elem` ("-._" :: String)
+  
+
+-- |
+--
+validateCommandArg :: String -> AppContext String
+validateCommandArg arg = do
+  let tArg = T.pack arg
+  when (hasDangerousChars tArg) $
+    throwError $ "Argument contains potentially dangerous characters: " <> arg
+  return arg
+  where
+    hasDangerousChars :: T.Text -> Bool
+    hasDangerousChars txt =
+      any (`T.isInfixOf` txt) [";", "&&", "|", "$", "`", "<", ">", "\\", "\""]
+
+-- |
+--
+validateCommandArgs :: [String] -> AppContext [String]
+validateCommandArgs = mapM validateCommandArg
+
+
+-- |
+--
+validateMessage :: String -> IO String
+validateMessage cmd = do
+  when (any (`elem` forbiddenChars) cmd) $
+    E.throwString "Command contains forbidden characters."
+
+  case words cmd of
+    (firstWord : _) -> when (firstWord `elem` forbiddenCommands) $
+                        E.throwString "Command is forbidden."
+    _ -> return ()
+
+  return cmd
+  where
+    forbiddenChars :: [Char]
+    forbiddenChars = [';', '&', '|', '`']
+
+    forbiddenCommands :: [String]
+    forbiddenCommands = ["rm", "mv", "dd", "chmod", "chown", "shutdown", "reboot", "kill", "nc", "telnet", "ssh"]
+
