diff --git a/README.md b/README.md
index 74c42296..36043eb2 100644
--- a/README.md
+++ b/README.md
@@ -118,6 +118,10 @@ OCO_LANGUAGE=<locale, scroll to the bottom to see options>
 OCO_MESSAGE_TEMPLATE_PLACEHOLDER=<message template placeholder, default: '$msg'>
 OCO_PROMPT_MODULE=<either conventional-commit or @commitlint, default: conventional-commit>
 OCO_ONE_LINE_COMMIT=<one line commit message, default: false>
+OCO_ENABLE_LOGGING=<boolean, enable/disable logging, default: true>
+OCO_ENABLE_CACHE=<boolean, enable/disable commit message caching, default: true>
+OCO_CACHE_DIR=<custom directory for storing commit message cache>
+OCO_LOG_DIR=<custom directory for storing log files>
 ```
 
 Global configs are same as local configs, but they are stored in the global `~/.opencommit` config file and set with `oco config set` command, e.g. `oco config set OCO_MODEL=gpt-4o`.
@@ -205,6 +209,52 @@ oco config set OCO_LANGUAGE=française
 The default language setting is **English**
 All available languages are currently listed in the [i18n](https://github.com/di-sukharev/opencommit/tree/master/src/i18n) folder
 
+### Logging and Caching Configuration
+
+OpenCommit provides options to control logging and caching behavior. These features help with debugging, performance optimization, and pre-commit hook integration.
+
+#### Logging Configuration
+
+You can enable/disable logging and customize the log directory:
+
+```sh
+# Disable logging
+oco config set OCO_ENABLE_LOGGING=false
+
+# Set custom log directory
+oco config set OCO_LOG_DIR=/path/to/custom/logs
+```
+
+By default:
+- Logs are stored in `~/.cache/opencommit/logs/` directory
+- Logs include commit generation process, errors, and pre-commit hook results
+- Each log entry is timestamped and categorized by level (INFO, ERROR, etc.)
+
+#### Caching Configuration
+
+OpenCommit caches commit messages to improve performance and provide better suggestions, especially useful during pre-commit hook operations:
+
+```sh
+# Disable commit message caching
+oco config set OCO_ENABLE_CACHE=false
+
+# Set custom cache directory
+oco config set OCO_CACHE_DIR=/path/to/custom/cache
+```
+
+By default:
+- Caching is enabled
+- Cache files are stored in `~/.cache/opencommit/` directory
+- Cache entries expire after 7 days
+- Each repository has its own cache file
+- Cache includes successful commit messages and their associated file changes
+- Cache is automatically cleared after successful commits
+- Cache helps speed up pre-commit hook operations by reusing recent commit messages
+
+The cache system is cross-platform compatible and follows XDG Base Directory specifications on Linux systems.
+
+> Note: When using pre-commit hooks, logging and caching are particularly useful for debugging issues and improving performance. The log files contain detailed information about hook execution and any failures that might occur.
+
 ### Push to git (gonna be deprecated)
 
 A prompt for pushing to git is on by default but if you would like to turn it off just use:
@@ -280,7 +330,7 @@ git commit -m "${generatedMessage}" --no-verify
 To include a message in the generated message, you can utilize the template function, for instance:
 
 ```sh
-oco '#205: $msg’
+oco '#205: $msg'
 ```
 
 > opencommit examines placeholders in the parameters, allowing you to append additional information before and after the placeholders, such as the relevant Issue or Pull Request. Similarly, you have the option to customize the OCO_MESSAGE_TEMPLATE_PLACEHOLDER configuration item, for example, simplifying it to $m!"
@@ -306,7 +356,7 @@ This line is responsible for replacing the placeholder in the `messageTemplate`
 
 #### Usage
 
-For instance, using the command `oco '$msg #205’`, users can leverage this feature. The provided code represents the backend mechanics of such commands, ensuring that the placeholder is replaced with the appropriate commit message.
+For instance, using the command `oco '$msg #205'`, users can leverage this feature. The provided code represents the backend mechanics of such commands, ensuring that the placeholder is replaced with the appropriate commit message.
 
 #### Committing with the Message
 
diff --git a/src/commands/commit.ts b/src/commands/commit.ts
index 1e7f2af6..04d6b9e2 100644
--- a/src/commands/commit.ts
+++ b/src/commands/commit.ts
@@ -19,6 +19,8 @@ import {
 } from '../utils/git';
 import { trytm } from '../utils/trytm';
 import { getConfig } from './config';
+import { CommitCache } from '../utils/commitCache';
+import { Logger } from '../utils/logger';
 
 const config = getConfig();
 
@@ -54,6 +56,7 @@ const generateCommitMessageFromGitDiff = async ({
   await assertGitRepo();
   const commitGenerationSpinner = spinner();
   commitGenerationSpinner.start('Generating the commit message');
+  Logger.spinner('Generating the commit message');
 
   try {
     let commitMessage = await generateCommitMessageByDiff(
@@ -61,6 +64,7 @@ const generateCommitMessageFromGitDiff = async ({
       fullGitMojiSpec,
       context
     );
+    Logger.debug('Generated commit message:', commitMessage);
 
     const messageTemplate = checkMessageTemplate(extraArgs);
     if (
@@ -74,110 +78,137 @@ const generateCommitMessageFromGitDiff = async ({
         config.OCO_MESSAGE_TEMPLATE_PLACEHOLDER,
         commitMessage
       );
+      Logger.debug('Applied message template:', commitMessage);
     }
 
     commitGenerationSpinner.stop('πŸ“ Commit message generated');
+    Logger.spinnerSuccess('Commit message generated');
 
-    outro(
-      `Generated commit message:
-${chalk.grey('β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”')}
-${commitMessage}
-${chalk.grey('β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”')}`
-    );
+    const separator = '─'.repeat(50);
+    const messageBox = `Generated commit message:\nβ”‚ ${chalk.grey(separator)}\nβ”‚ ${commitMessage}\nβ”‚ ${chalk.grey(separator)}`;
+    outro(messageBox);
+    // Remove duplicate logging
+    Logger.debug('Generated commit message:', commitMessage);
 
     const isCommitConfirmedByUser =
       skipCommitConfirmation ||
       (await confirm({
-        message: 'Confirm the commit message?'
+        message: `Confirm the commit message?`
       }));
 
-    if (isCancel(isCommitConfirmedByUser)) process.exit(1);
+    if (isCancel(isCommitConfirmedByUser)) {
+      Logger.info('User cancelled commit');
+      process.exit(0);
+    }
 
     if (isCommitConfirmedByUser) {
+      Logger.info('User confirmed commit message');
+      // Save commit message to cache before committing
+      await CommitCache.saveCommitMessage(commitMessage);
+      Logger.debug('Saved commit message to cache');
+
       const committingChangesSpinner = spinner();
       committingChangesSpinner.start('Committing the changes');
-      const { stdout } = await execa('git', [
-        'commit',
-        '-m',
-        commitMessage,
-        ...extraArgs
-      ]);
-      committingChangesSpinner.stop(
-        `${chalk.green('βœ”')} Successfully committed`
-      );
-
-      outro(stdout);
-
-      const remotes = await getGitRemotes();
-
-      // user isn't pushing, return early
-      if (config.OCO_GITPUSH === false) return;
-
-      if (!remotes.length) {
-        const { stdout } = await execa('git', ['push']);
-        if (stdout) outro(stdout);
-        process.exit(0);
-      }
+      Logger.spinner('Committing the changes');
+
+      try {
+        const { stdout } = await execa('git', [
+          'commit',
+          '-m',
+          commitMessage,
+          ...extraArgs
+        ]);
+        const successMessage = `${chalk.green('βœ”')} Successfully committed`;
+        committingChangesSpinner.stop(successMessage);
+        Logger.spinnerSuccess('Successfully committed');
+        
+        await CommitCache.clearCache();
+        Logger.debug('Cleared commit cache');
+        
+        // Only output git commit result once
+        outro(`β”‚ ${stdout}`);
+
+        const remotes = await getGitRemotes();
+
+        // user isn't pushing, return early
+        if (config.OCO_GITPUSH === false) return;
+
+        if (!remotes.length) {
+          const { stdout } = await execa('git', ['push']);
+          if (stdout) outro(stdout);
+          process.exit(0);
+        }
 
-      if (remotes.length === 1) {
-        const isPushConfirmedByUser = await confirm({
-          message: 'Do you want to run `git push`?'
-        });
+        if (remotes.length === 1) {
+          const isPushConfirmedByUser = await confirm({
+            message: 'Do you want to run `git push`?'
+          });
 
-        if (isCancel(isPushConfirmedByUser)) process.exit(1);
+          if (isCancel(isPushConfirmedByUser)) process.exit(1);
 
-        if (isPushConfirmedByUser) {
-          const pushSpinner = spinner();
+          if (isPushConfirmedByUser) {
+            const pushSpinner = spinner();
 
-          pushSpinner.start(`Running 'git push ${remotes[0]}'`);
+            pushSpinner.start(`Running 'git push ${remotes[0]}'`);
 
-          const { stdout } = await execa('git', [
-            'push',
-            '--verbose',
-            remotes[0]
-          ]);
+            const { stdout } = await execa('git', [
+              'push',
+              '--verbose',
+              remotes[0]
+            ]);
 
-          pushSpinner.stop(
-            `${chalk.green('βœ”')} Successfully pushed all commits to ${remotes[0]
-            }`
-          );
+            pushSpinner.stop(
+              `β”‚ ${chalk.green('βœ”')} Successfully pushed all commits to ${remotes[0]}`
+            );
 
-          if (stdout) outro(stdout);
+            if (stdout) outro(`β”‚\nβ”‚ ${stdout}\nβ”‚`);
+          } else {
+            outro('β”‚\nβ”‚ `git push` aborted\nβ”‚');
+            process.exit(0);
+          }
         } else {
-          outro('`git push` aborted');
-          process.exit(0);
-        }
-      } else {
-        const skipOption = `don't push`
-        const selectedRemote = (await select({
-          message: 'Choose a remote to push to',
-          options: [...remotes, skipOption].map((remote) => ({ value: remote, label: remote })),
-        })) as string;
-
-        if (isCancel(selectedRemote)) process.exit(1);
-
-        if (selectedRemote !== skipOption) {
-          const pushSpinner = spinner();
-  
-          pushSpinner.start(`Running 'git push ${selectedRemote}'`);
-  
-          const { stdout } = await execa('git', ['push', selectedRemote]);
-  
-          if (stdout) outro(stdout);
-  
-          pushSpinner.stop(
-            `${chalk.green(
-              'βœ”'
-            )} successfully pushed all commits to ${selectedRemote}`
-          );
+          const skipOption = `don't push`
+          const selectedRemote = (await select({
+            message: 'Choose a remote to push to:',
+            options: [...remotes, skipOption].map((remote) => ({
+              value: remote,
+              label: remote
+            }))
+          })) as string;
+
+          if (isCancel(selectedRemote)) process.exit(1);
+
+          if (selectedRemote !== skipOption) {
+            const pushSpinner = spinner();
+    
+            pushSpinner.start(`Running 'git push ${selectedRemote}'`);
+    
+            const { stdout } = await execa('git', ['push', selectedRemote]);
+    
+            if (stdout) outro(`β”‚\nβ”‚ ${stdout}\nβ”‚`);
+    
+            pushSpinner.stop(
+              `β”‚ ${chalk.green('βœ”')} Successfully pushed all commits to ${selectedRemote}`
+            );
+          }
         }
+      } catch (error: any) {
+        const failMessage = `${chalk.red('βœ–')} Commit failed`;
+        committingChangesSpinner.stop(failMessage);
+        Logger.spinnerError('Commit failed');
+        
+        outro(formatErrorOutput(error, 'Commit Failed'));
+        // Remove duplicate logging
+        Logger.debug('Commit error:', error);
+        
+        process.exit(1);
       }
     } else {
       const regenerateMessage = await confirm({
         message: 'Do you want to regenerate the message?'
       });
 
-      if (isCancel(regenerateMessage)) process.exit(1);
+      if (isCancel(regenerateMessage)) process.exit(0);
 
       if (regenerateMessage) {
         await generateCommitMessageFromGitDiff({
@@ -185,19 +216,96 @@ ${chalk.grey('β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”β€”')}`
           extraArgs,
           fullGitMojiSpec
         });
+      } else {
+        process.exit(0);
       }
     }
-  } catch (error) {
-    commitGenerationSpinner.stop(
-      `${chalk.red('βœ–')} Failed to generate the commit message`
-    );
+  } catch (error: any) {
+    const failMessage = `${chalk.red('βœ–')} Failed to generate the commit message`;
+    commitGenerationSpinner.stop(failMessage);
+    Logger.spinnerError('Failed to generate the commit message');
+    
+    outro(formatErrorOutput(error, 'Commit Failed'));
+    // Remove duplicate logging
+    Logger.debug('Generation error:', error);
+    
+    process.exit(1);
+  }
+};
 
-    console.log(error);
+const formatErrorOutput = (error: any, type: string = 'Commit Failed') => {
+  const separator = chalk.gray('─'.repeat(50));
+  const formatLine = (line: string) => {
+    const trimmed = line.trim();
+    if (!trimmed) return 'β”‚';
+    
+    // Format check results with proper alignment
+    if (trimmed.match(/\.(Passed|Failed|Skipped)$/)) {
+      const [name, result] = trimmed.split(/\.{2,}/);
+      const dots = '.'.repeat(Math.max(0, 50 - name.length - result.length));
+      const formattedResult = result.trim();
+      
+      if (formattedResult === 'Failed') {
+        return `β”‚ ${name}${dots}${chalk.red(formattedResult)}`;
+      }
+      if (formattedResult === 'Passed') {
+        return `β”‚ ${name}${dots}${chalk.green(formattedResult)}`;
+      }
+      if (formattedResult === 'Skipped') {
+        return `β”‚ ${name}${dots}${chalk.gray(formattedResult)}`;
+      }
+    }
 
-    const err = error as Error;
-    outro(`${chalk.red('βœ–')} ${err?.message || err}`);
-    process.exit(1);
+    // Format error details with indentation
+    if (trimmed.startsWith('-')) {
+      return `β”‚   ${chalk.gray(trimmed)}`;
+    }
+
+    // Format file paths and error messages
+    if (trimmed.includes(':')) {
+      return `β”‚   ${trimmed.replace(/([^:]+):(\d+)?/g, (_, file, line) => {
+        return chalk.cyan(file) + (line ? chalk.yellow(':' + line) : '');
+      })}`;
+    }
+
+    // Format warnings and info
+    if (trimmed.includes('[WARNING]')) return `β”‚ ${chalk.yellow(trimmed)}`;
+    if (trimmed.includes('[INFO]')) return `β”‚ ${chalk.blue(trimmed)}`;
+
+    return `β”‚ ${trimmed}`;
+  };
+
+  const isPreCommitError = type === 'Pre-commit Hook Failed';
+  const lines = [
+    chalk.red(type),
+    separator
+  ];
+
+  if (error.stderr) {
+    const formattedLines = error.stderr
+      .split('\n')
+      .map(formatLine)
+      .filter(line => line.length > 0);
+    
+    lines.push(...formattedLines);
+  } else {
+    lines.push(`β”‚ ${chalk.red(error.message || String(error))}`);
+  }
+
+  lines.push(
+    separator,
+    'β”‚ ' + chalk.yellow(isPreCommitError 
+      ? 'Please fix the issues in your code before committing'
+      : 'Fix the issues or generate a new commit message'
+    ),
+    'β”‚ ' + `Logs: ${Logger.getLogPath()}`
+  );
+
+  if (!isPreCommitError) {
+    lines.push('β”‚ ' + chalk.gray('Consider generating a new commit message if the issue persists'));
   }
+
+  return lines.join('\n');
 };
 
 export async function commit(
@@ -207,85 +315,149 @@ export async function commit(
   fullGitMojiSpec: boolean = false,
   skipCommitConfirmation: boolean = false
 ) {
-  if (isStageAllFlag) {
-    const changedFiles = await getChangedFiles();
+  try {
+    if (isStageAllFlag) {
+      const changedFiles = await getChangedFiles();
 
-    if (changedFiles) await gitAdd({ files: changedFiles });
-    else {
-      outro('No changes detected, write some code and run `oco` again');
+      if (changedFiles) await gitAdd({ files: changedFiles });
+      else {
+        outro('No changes detected, write some code and run `oco` again');
+        process.exit(0);
+      }
+    }
+
+    const [stagedFiles, errorStagedFiles] = await trytm(getStagedFiles());
+    const [changedFiles, errorChangedFiles] = await trytm(getChangedFiles());
+
+    if (!changedFiles?.length && !stagedFiles?.length) {
+      outro(chalk.red('No changes detected'));
+      process.exit(0);
+    }
+
+    intro('open-commit');
+    if (errorChangedFiles ?? errorStagedFiles) {
+      outro(`${chalk.red('βœ–')} ${errorChangedFiles ?? errorStagedFiles}`);
       process.exit(1);
     }
-  }
 
-  const [stagedFiles, errorStagedFiles] = await trytm(getStagedFiles());
-  const [changedFiles, errorChangedFiles] = await trytm(getChangedFiles());
+    const stagedFilesSpinner = spinner();
+    stagedFilesSpinner.start('Counting staged files');
 
-  if (!changedFiles?.length && !stagedFiles?.length) {
-    outro(chalk.red('No changes detected'));
-    process.exit(1);
-  }
+    // Clean up old caches periodically
+    await CommitCache.cleanupOldCaches();
 
-  intro('open-commit');
-  if (errorChangedFiles ?? errorStagedFiles) {
-    outro(`${chalk.red('βœ–')} ${errorChangedFiles ?? errorStagedFiles}`);
-    process.exit(1);
-  }
+    // Check for cached commit message
+    const cachedCommit = await CommitCache.getLastCommitMessage();
+    if (cachedCommit) {
+      // Stop the spinner before showing the cached message prompt
+      stagedFilesSpinner.stop('Files counted');
+      Logger.spinner('Files counted');
+
+      Logger.info('Found cached commit message');
+      const separator = '─'.repeat(50);
+      const useCachedMessage = await confirm({
+        message: `Found cached commit message for the same files. Use it?\nβ”‚ ${chalk.grey(separator)}\nβ”‚ ${cachedCommit.message}\nβ”‚ ${chalk.grey(separator)}`
+      });
+
+      if (isCancel(useCachedMessage)) {
+        Logger.info('User cancelled using cached message');
+        process.exit(0);
+      }
 
-  const stagedFilesSpinner = spinner();
+      if (useCachedMessage) {
+        Logger.info('Using cached commit message');
+        const committingChangesSpinner = spinner();
+        
+        try {
+          // Start the commit spinner
+          committingChangesSpinner.start('Committing with cached message');
+          Logger.spinner('Committing with cached message');
 
-  stagedFilesSpinner.start('Counting staged files');
+          const { stdout } = await execa('git', [
+            'commit',
+            '-m',
+            cachedCommit.message,
+            ...extraArgs
+          ]);
 
-  if (!stagedFiles.length) {
-    stagedFilesSpinner.stop('No files are staged');
-    const isStageAllAndCommitConfirmedByUser = await confirm({
-      message: 'Do you want to stage all files and generate commit message?'
-    });
+          const successMessage = `${chalk.green('βœ”')} Successfully committed with cached message`;
+          committingChangesSpinner.stop(successMessage);
+          Logger.spinnerSuccess('Successfully committed with cached message');
 
-    if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
+          await CommitCache.clearCache();
+          Logger.debug('Cleared commit cache');
 
-    if (isStageAllAndCommitConfirmedByUser) {
-      await commit(extraArgs, context, true, fullGitMojiSpec);
-      process.exit(1);
+          // Only output git commit result once
+          outro(`β”‚ ${stdout}`);
+          
+          process.exit(0);
+        } catch (error: any) {
+          const failMessage = `${chalk.red('βœ–')} Commit failed`;
+          committingChangesSpinner.stop(failMessage);
+          Logger.spinnerError('Commit failed with cached message');
+          
+          outro(formatErrorOutput(error, 'Pre-commit Hook Failed'));
+          
+          process.exit(1);
+        }
+      }
     }
 
-    if (stagedFiles.length === 0 && changedFiles.length > 0) {
-      const files = (await multiselect({
-        message: chalk.cyan('Select the files you want to add to the commit:'),
-        options: changedFiles.map((file) => ({
-          value: file,
-          label: file
-        }))
-      })) as string[];
+    if (!stagedFiles.length) {
+      stagedFilesSpinner.stop('No files are staged');
+      const isStageAllAndCommitConfirmedByUser = await confirm({
+        message: 'Do you want to stage all files and generate commit message?'
+      });
+
+      if (isCancel(isStageAllAndCommitConfirmedByUser)) process.exit(1);
+
+      if (isStageAllAndCommitConfirmedByUser) {
+        await commit(extraArgs, context, true, fullGitMojiSpec);
+        process.exit(1);
+      }
+
+      if (stagedFiles.length === 0 && changedFiles.length > 0) {
+        const files = (await multiselect({
+          message: chalk.cyan('Select the files you want to add to the commit:'),
+          options: changedFiles.map((file) => ({
+            value: file,
+            label: `  ${file}`
+          }))
+        })) as string[];
 
-      if (isCancel(files)) process.exit(1);
+        if (isCancel(files)) process.exit(1);
 
-      await gitAdd({ files });
+        await gitAdd({ files });
+      }
+
+      await commit(extraArgs, context, false, fullGitMojiSpec);
+      process.exit(1);
     }
 
-    await commit(extraArgs, context, false, fullGitMojiSpec);
-    process.exit(1);
-  }
+    stagedFilesSpinner.stop(
+      `${stagedFiles.length} staged files:\n${stagedFiles
+        .map((file) => `  ${file}`)
+        .join('\n')}`
+    );
 
-  stagedFilesSpinner.stop(
-    `${stagedFiles.length} staged files:\n${stagedFiles
-      .map((file) => `  ${file}`)
-      .join('\n')}`
-  );
+    const [, generateCommitError] = await trytm(
+      generateCommitMessageFromGitDiff({
+        diff: await getDiff({ files: stagedFiles }),
+        extraArgs,
+        context,
+        fullGitMojiSpec,
+        skipCommitConfirmation
+      })
+    );
 
-  const [, generateCommitError] = await trytm(
-    generateCommitMessageFromGitDiff({
-      diff: await getDiff({ files: stagedFiles }),
-      extraArgs,
-      context,
-      fullGitMojiSpec,
-      skipCommitConfirmation
-    })
-  );
+    if (generateCommitError) {
+      outro(chalk.red(generateCommitError));
+      process.exit(1);
+    }
 
-  if (generateCommitError) {
-    outro(`${chalk.red('βœ–')} ${generateCommitError}`);
+    process.exit(0);
+  } catch (error: any) {
+    outro(chalk.red(error.message || String(error)));
     process.exit(1);
   }
-
-  process.exit(0);
 }
diff --git a/src/commands/config.ts b/src/commands/config.ts
index 169ee3cd..b5e9cf13 100644
--- a/src/commands/config.ts
+++ b/src/commands/config.ts
@@ -25,6 +25,10 @@ export enum CONFIG_KEYS {
   OCO_ONE_LINE_COMMIT = 'OCO_ONE_LINE_COMMIT',
   OCO_TEST_MOCK_TYPE = 'OCO_TEST_MOCK_TYPE',
   OCO_API_URL = 'OCO_API_URL',
+  OCO_ENABLE_LOGGING = 'OCO_ENABLE_LOGGING',
+  OCO_ENABLE_CACHE = 'OCO_ENABLE_CACHE',
+  OCO_CACHE_DIR = 'OCO_CACHE_DIR',
+  OCO_LOG_DIR = 'OCO_LOG_DIR'
   OCO_OMIT_SCOPE = 'OCO_OMIT_SCOPE',
   OCO_GITPUSH = 'OCO_GITPUSH' // todo: deprecate
 }
@@ -392,6 +396,10 @@ export type ConfigType = {
   [CONFIG_KEYS.OCO_ONE_LINE_COMMIT]: boolean;
   [CONFIG_KEYS.OCO_OMIT_SCOPE]: boolean;
   [CONFIG_KEYS.OCO_TEST_MOCK_TYPE]: string;
+  [CONFIG_KEYS.OCO_ENABLE_LOGGING]?: boolean;
+  [CONFIG_KEYS.OCO_ENABLE_CACHE]?: boolean;
+  [CONFIG_KEYS.OCO_CACHE_DIR]?: string;
+  [CONFIG_KEYS.OCO_LOG_DIR]?: string;
 };
 
 export const defaultConfigPath = pathJoin(homedir(), '.opencommit');
@@ -438,6 +446,10 @@ export const DEFAULT_CONFIG = {
   OCO_ONE_LINE_COMMIT: false,
   OCO_TEST_MOCK_TYPE: 'commit-message',
   OCO_WHY: false,
+  OCO_ENABLE_LOGGING: true,
+  OCO_ENABLE_CACHE: true,
+  OCO_CACHE_DIR: '',
+  OCO_LOG_DIR: ''
   OCO_OMIT_SCOPE: false,
   OCO_GITPUSH: true // todo: deprecate
 };
@@ -479,7 +491,11 @@ const getEnvConfig = (envPath: string) => {
     OCO_TEST_MOCK_TYPE: process.env.OCO_TEST_MOCK_TYPE,
     OCO_OMIT_SCOPE: parseConfigVarValue(process.env.OCO_OMIT_SCOPE),
 
-    OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH) // todo: deprecate
+    OCO_GITPUSH: parseConfigVarValue(process.env.OCO_GITPUSH),
+    OCO_ENABLE_LOGGING: parseConfigVarValue(process.env.OCO_ENABLE_LOGGING),
+    OCO_ENABLE_CACHE: parseConfigVarValue(process.env.OCO_ENABLE_CACHE),
+    OCO_CACHE_DIR: parseConfigVarValue(process.env.OCO_CACHE_DIR),
+    OCO_LOG_DIR: parseConfigVarValue(process.env.OCO_LOG_DIR)
   };
 };
 
diff --git a/src/utils/commitCache.ts b/src/utils/commitCache.ts
new file mode 100644
index 00000000..3f33db8d
--- /dev/null
+++ b/src/utils/commitCache.ts
@@ -0,0 +1,168 @@
+import { homedir } from 'os';
+import { join } from 'path';
+import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
+import { execa } from 'execa';
+import { createHash } from 'crypto';
+import { getConfig } from '../commands/config';
+
+export interface CommitCacheData {
+  message: string;
+  timestamp: number;
+  files: string[];
+  repoPath: string;
+}
+
+export class CommitCache {
+  private static readonly DEFAULT_CACHE_HOME = process.env.XDG_CACHE_HOME || join(homedir(), '.cache');
+  private static readonly DEFAULT_CACHE_DIR = join(CommitCache.DEFAULT_CACHE_HOME, 'opencommit');
+
+  private static getCacheDir(): string {
+    const config = getConfig();
+    return config.OCO_CACHE_DIR || CommitCache.DEFAULT_CACHE_DIR;
+  }
+
+  private static isCacheEnabled(): boolean {
+    const config = getConfig();
+    return config.OCO_ENABLE_CACHE !== false;
+  }
+
+  private static async getRepoRoot(): Promise<string> {
+    try {
+      const { stdout } = await execa('git', ['rev-parse', '--show-toplevel']);
+      return stdout.trim();
+    } catch (error) {
+      throw new Error('Not a git repository');
+    }
+  }
+
+  private static getCacheFilePath(repoPath: string): string {
+    const repoHash = createHash('md5').update(repoPath).digest('hex');
+    return join(this.getCacheDir(), `${repoHash}_commit_cache.json`);
+  }
+
+  private static ensureCacheDir() {
+    if (!this.isCacheEnabled()) return;
+
+    const cacheDir = this.getCacheDir();
+    if (!existsSync(cacheDir)) {
+      mkdirSync(cacheDir, { recursive: true });
+    }
+  }
+
+  private static async getStagedFiles(): Promise<string[]> {
+    try {
+      const { stdout } = await execa('git', ['diff', '--cached', '--name-only']);
+      return stdout.split('\n').filter(Boolean);
+    } catch (error) {
+      console.error('Error getting staged files:', error);
+      return [];
+    }
+  }
+
+  static async saveCommitMessage(message: string) {
+    if (!this.isCacheEnabled()) return;
+
+    this.ensureCacheDir();
+    const repoPath = await this.getRepoRoot();
+    const files = await this.getStagedFiles();
+    
+    const cacheData: CommitCacheData = {
+      message,
+      timestamp: Date.now(),
+      files,
+      repoPath
+    };
+    writeFileSync(this.getCacheFilePath(repoPath), JSON.stringify(cacheData, null, 2));
+  }
+
+  static async getLastCommitMessage(): Promise<CommitCacheData | null> {
+    if (!this.isCacheEnabled()) return null;
+
+    try {
+      const repoPath = await this.getRepoRoot();
+      const cacheFilePath = this.getCacheFilePath(repoPath);
+      
+      if (!existsSync(cacheFilePath)) {
+        return null;
+      }
+
+      const cacheContent = readFileSync(cacheFilePath, 'utf-8');
+      if (!cacheContent.trim()) {
+        return null;
+      }
+
+      const cacheData = JSON.parse(cacheContent) as CommitCacheData;
+
+      if (cacheData.repoPath !== repoPath) {
+        return null;
+      }
+
+      const currentFiles = await this.getStagedFiles();
+      const cachedFileSet = new Set(cacheData.files);
+      const currentFileSet = new Set(currentFiles);
+      
+      if (cachedFileSet.size !== currentFileSet.size) {
+        return null;
+      }
+      
+      for (const file of currentFileSet) {
+        if (!cachedFileSet.has(file)) {
+          return null;
+        }
+      }
+
+      return cacheData;
+    } catch (error) {
+      console.error('Error reading commit cache:', error);
+      return null;
+    }
+  }
+
+  static async clearCache() {
+    if (!this.isCacheEnabled()) return;
+
+    try {
+      const repoPath = await this.getRepoRoot();
+      const cacheFilePath = this.getCacheFilePath(repoPath);
+      if (existsSync(cacheFilePath)) {
+        writeFileSync(cacheFilePath, JSON.stringify({}, null, 2));
+      }
+    } catch (error) {
+      console.error('Error clearing commit cache:', error);
+    }
+  }
+
+  static async cleanupOldCaches() {
+    if (!this.isCacheEnabled()) return;
+
+    try {
+      const cacheDir = this.getCacheDir();
+      if (!existsSync(cacheDir)) {
+        return;
+      }
+
+      const files = readdirSync(cacheDir);
+      const now = Date.now();
+      const maxAge = 7 * 24 * 60 * 60 * 1000; // 7 days
+
+      for (const file of files) {
+        if (!file.endsWith('_commit_cache.json')) continue;
+
+        const filePath = join(cacheDir, file);
+        try {
+          const content = readFileSync(filePath, 'utf-8');
+          if (!content.trim()) continue;
+
+          const cacheData = JSON.parse(content) as CommitCacheData;
+          if (now - cacheData.timestamp > maxAge) {
+            writeFileSync(filePath, JSON.stringify({}, null, 2));
+          }
+        } catch (error) {
+          writeFileSync(filePath, JSON.stringify({}, null, 2));
+        }
+      }
+    } catch (error) {
+      console.error('Error cleaning up old caches:', error);
+    }
+  }
+} 
\ No newline at end of file
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
new file mode 100644
index 00000000..186e64fc
--- /dev/null
+++ b/src/utils/logger.ts
@@ -0,0 +1,119 @@
+import { homedir } from 'os';
+import { join } from 'path';
+import { existsSync, mkdirSync, appendFileSync } from 'fs';
+import { format } from 'util';
+import chalk from 'chalk';
+import { getConfig } from '../commands/config';
+
+export class Logger {
+  private static readonly DEFAULT_LOG_DIR = join(homedir(), '.cache', 'opencommit', 'logs');
+  private static readonly DEFAULT_LOG_FILE = join(Logger.DEFAULT_LOG_DIR, 'opencommit.log');
+
+  private static getLogDir(): string {
+    const config = getConfig();
+    return config.OCO_LOG_DIR || Logger.DEFAULT_LOG_DIR;
+  }
+
+  private static getLogFile(): string {
+    return join(this.getLogDir(), 'opencommit.log');
+  }
+
+  private static isLoggingEnabled(): boolean {
+    const config = getConfig();
+    return config.OCO_ENABLE_LOGGING !== false;
+  }
+
+  private static ensureLogDir() {
+    if (!this.isLoggingEnabled()) return;
+    
+    const logDir = this.getLogDir();
+    if (!existsSync(logDir)) {
+      mkdirSync(logDir, { recursive: true });
+    }
+  }
+
+  private static getTimestamp(): string {
+    return new Date().toISOString();
+  }
+
+  private static stripAnsi(str: string): string {
+    // Remove ANSI color codes
+    return str.replace(/\x1B\[\d+m/g, '');
+  }
+
+  private static writeToLog(level: string, message: string) {
+    if (!this.isLoggingEnabled()) return;
+    
+    this.ensureLogDir();
+    const timestamp = this.getTimestamp();
+    const cleanMessage = this.stripAnsi(message);
+    const logEntry = `[${timestamp}] [${level}] ${cleanMessage}\n`;
+    appendFileSync(this.getLogFile(), logEntry);
+  }
+
+  private static formatMultilineMessage(message: string): string {
+    return message.split('\n')
+      .map(line => line.trim())
+      .filter(line => line.length > 0)
+      .map(line => `β”‚ ${line}`)
+      .join('\n');
+  }
+
+  static info(message: string, ...args: any[]) {
+    const formattedMessage = format(message, ...args);
+    this.writeToLog('INFO', formattedMessage);
+    console.log(this.formatMultilineMessage(formattedMessage));
+  }
+
+  static error(message: string, ...args: any[]) {
+    const formattedMessage = format(message, ...args);
+    this.writeToLog('ERROR', formattedMessage);
+    console.error(this.formatMultilineMessage(chalk.red(formattedMessage)));
+  }
+
+  static warn(message: string, ...args: any[]) {
+    const formattedMessage = format(message, ...args);
+    this.writeToLog('WARN', formattedMessage);
+    console.warn(this.formatMultilineMessage(chalk.yellow(formattedMessage)));
+  }
+
+  static debug(message: string, ...args: any[]) {
+    if (process.env.OCO_DEBUG) {
+      const formattedMessage = format(message, ...args);
+      this.writeToLog('DEBUG', formattedMessage);
+      console.debug(this.formatMultilineMessage(chalk.gray(formattedMessage)));
+    }
+  }
+
+  static spinner(message: string) {
+    this.writeToLog('INFO', `[SPINNER] ${message}`);
+  }
+
+  static spinnerSuccess(message: string) {
+    this.writeToLog('INFO', `[SPINNER_SUCCESS] ${message}`);
+  }
+
+  static spinnerError(message: string) {
+    this.writeToLog('ERROR', `[SPINNER_ERROR] ${message}`);
+  }
+
+  static commitError(error: any) {
+    if (error.stderr) {
+      const lines = error.stderr.split('\n')
+        .map(line => line.trim())
+        .filter(line => line.length > 0);
+      // Only write to log file, don't print to console
+      lines.forEach(line => {
+        this.writeToLog('ERROR', line);
+      });
+    }
+    if (error.message) {
+      // Only write to log file, don't print to console
+      this.writeToLog('ERROR', error.message);
+    }
+  }
+
+  static getLogPath(): string {
+    return this.isLoggingEnabled() ? this.getLogFile() : 'Logging is disabled';
+  }
+} 
\ No newline at end of file
diff --git a/test/unit/commitCache.test.ts b/test/unit/commitCache.test.ts
new file mode 100644
index 00000000..8c5f8b74
--- /dev/null
+++ b/test/unit/commitCache.test.ts
@@ -0,0 +1,140 @@
+import { CommitCache } from '../../src/utils/commitCache';
+import { join } from 'path';
+import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, rmdirSync } from 'fs';
+import { execSync } from 'child_process';
+
+describe('CommitCache', () => {
+  const TEST_DIR = join(process.cwd(), 'test-repos');
+  const TEST_REPO = join(TEST_DIR, 'test-repo');
+  let originalCwd: string;
+
+  beforeAll(() => {
+    // Save original working directory
+    originalCwd = process.cwd();
+    
+    // Create test directory and repository
+    execSync(`
+      mkdir -p "${TEST_REPO}"
+      cd "${TEST_REPO}"
+      git init
+      git config user.name "test"
+      git config user.email "test@test.com"
+      echo "test" > test.txt
+      git add test.txt
+    `);
+
+    // Ensure cache is enabled for tests
+    process.env.OCO_ENABLE_CACHE = 'true';
+  });
+
+  beforeEach(() => {
+    // Change to test repository directory
+    process.chdir(TEST_REPO);
+  });
+
+  afterEach(() => {
+    // Return to original directory
+    process.chdir(originalCwd);
+  });
+
+  afterAll(() => {
+    // Clean up test repositories
+    execSync(`rm -rf "${TEST_DIR}"`);
+    delete process.env.OCO_ENABLE_CACHE;
+  });
+
+  it('should save and retrieve commit message', async () => {
+    const testMessage = 'test commit message';
+
+    await CommitCache.saveCommitMessage(testMessage);
+    const cached = await CommitCache.getLastCommitMessage();
+
+    expect(cached).not.toBeNull();
+    expect(cached?.message).toBe(testMessage);
+    expect(cached?.files).toEqual(['test.txt']);
+  });
+
+  it('should not retrieve message with different files', async () => {
+    const testMessage = 'test commit message';
+
+    // Add a new file to change the staged files list
+    execSync('echo "different" > different.txt && git add different.txt');
+
+    await CommitCache.saveCommitMessage(testMessage);
+    
+    // Change back to original state
+    execSync('git reset different.txt && rm different.txt');
+    
+    const cached = await CommitCache.getLastCommitMessage();
+
+    expect(cached).toBeNull();
+  });
+
+  it('should clear cache after successful commit', async () => {
+    const testMessage = 'test commit message';
+
+    await CommitCache.saveCommitMessage(testMessage);
+    await CommitCache.clearCache();
+    const cached = await CommitCache.getLastCommitMessage();
+
+    expect(cached).toBeNull();
+  });
+
+  it('should handle multiple repositories', async () => {
+    // Create second test repository
+    const TEST_REPO2 = join(TEST_DIR, 'test-repo2');
+    execSync(`
+      mkdir -p "${TEST_REPO2}"
+      cd "${TEST_REPO2}"
+      git init
+      git config user.name "test"
+      git config user.email "test@test.com"
+      echo "test2" > test2.txt
+      git add test2.txt
+    `);
+
+    // Save message in first repo
+    process.chdir(TEST_REPO);
+    const testMessage1 = 'test commit message 1';
+    await CommitCache.saveCommitMessage(testMessage1);
+
+    // Save message in second repo
+    process.chdir(TEST_REPO2);
+    const testMessage2 = 'test commit message 2';
+    await CommitCache.saveCommitMessage(testMessage2);
+
+    // Check first repo cache
+    process.chdir(TEST_REPO);
+    const cached1 = await CommitCache.getLastCommitMessage();
+    expect(cached1?.message).toBe(testMessage1);
+    expect(cached1?.files).toEqual(['test.txt']);
+
+    // Check second repo cache
+    process.chdir(TEST_REPO2);
+    const cached2 = await CommitCache.getLastCommitMessage();
+    expect(cached2?.message).toBe(testMessage2);
+    expect(cached2?.files).toEqual(['test2.txt']);
+  });
+
+  it('should clean up old caches', async () => {
+    const testMessage = 'test commit message';
+
+    // Mock Date.now() to return a fixed timestamp
+    const realDateNow = Date.now;
+    const mockNow = new Date('2024-01-01').getTime();
+    global.Date.now = jest.fn(() => mockNow);
+
+    // Save message with mocked timestamp
+    await CommitCache.saveCommitMessage(testMessage);
+
+    // Restore real Date.now
+    global.Date.now = realDateNow;
+
+    // Run cleanup with current time (which should be > 7 days after mocked time)
+    await CommitCache.cleanupOldCaches();
+    
+    // Try to retrieve the message
+    const cached = await CommitCache.getLastCommitMessage();
+    expect(cached).toBeNull();
+  });
+}); 
\ No newline at end of file